import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { execFileSync } from "child_process";
import { readFileSync, writeFileSync } from "fs";
import path from "path";
import { fileURLToPath } from "url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PROJECT_ROOT = path.resolve(__dirname, "../..");
const STACK_ENV = "stg";
const BASE_URL = process.env.BASE_URL ?? "";
const VERSION_PATH = process.env.VERSION_PATH ?? "/api/version-app";
const DEFAULT_STACK_NAME = process.env.STACK_NAME ?? "";
const GH_TOKEN = process.env.GH_TOKEN ?? "";

const GH_URL_RAW = process.env.GH_URL ?? "";
// support both "owner/repo" and "https://github.com/owner/repo" formats
const REPO = GH_URL_RAW.startsWith("http")
  ? GH_URL_RAW.replace(/^https?:\/\/[^/]+\//, "").replace(/\.git$/, "")
  : GH_URL_RAW;

const GIT = (args: string[]) =>
  execFileSync("git", args, { encoding: "utf-8", cwd: PROJECT_ROOT }).trim();

// --- GitHub API client (no gh CLI) ---

const GH_API = "https://api.github.com";

type WorkflowRun = {
  id: number;
  name: string;
  status: string;
  conclusion: string | null;
  html_url: string;
  created_at: string;
  run_started_at: string;
};

async function ghFetch<T = unknown>(
  pathname: string,
  init: RequestInit = {}
): Promise<T> {
  if (!GH_TOKEN) throw new Error("GH_TOKEN tidak di-set.");
  if (!REPO) throw new Error("GH_URL tidak di-set.");

  const res = await fetch(`${GH_API}${pathname}`, {
    ...init,
    headers: {
      Accept: "application/vnd.github+json",
      Authorization: `Bearer ${GH_TOKEN}`,
      "X-GitHub-Api-Version": "2022-11-28",
      ...(init.body ? { "Content-Type": "application/json" } : {}),
      ...(init.headers ?? {}),
    },
  });

  if (!res.ok) {
    const body = await res.text();
    throw new Error(`GitHub API ${res.status} ${res.statusText}: ${body}`);
  }

  // 204 No Content (e.g. workflow dispatch)
  if (res.status === 204) return undefined as T;
  return (await res.json()) as T;
}

async function triggerWorkflow(
  workflow: string,
  ref: string,
  inputs: Record<string, string>
): Promise<void> {
  await ghFetch(`/repos/${REPO}/actions/workflows/${workflow}/dispatches`, {
    method: "POST",
    body: JSON.stringify({ ref, inputs }),
  });
}

async function getLatestRun(workflow: string): Promise<WorkflowRun> {
  const data = await ghFetch<{ workflow_runs: WorkflowRun[] }>(
    `/repos/${REPO}/actions/workflows/${workflow}/runs?per_page=1`
  );
  if (!data.workflow_runs?.length) {
    throw new Error(`Tidak ada run untuk workflow ${workflow}.`);
  }
  return data.workflow_runs[0];
}

async function listRuns(
  workflow: string | "all",
  limit: number
): Promise<WorkflowRun[]> {
  const url =
    workflow === "all"
      ? `/repos/${REPO}/actions/runs?per_page=${limit}`
      : `/repos/${REPO}/actions/workflows/${workflow}/runs?per_page=${limit}`;
  const data = await ghFetch<{ workflow_runs: WorkflowRun[] }>(url);
  return data.workflow_runs ?? [];
}

async function waitForRun(
  runId: number,
  timeoutMs = 30 * 60 * 1000
): Promise<WorkflowRun> {
  const interval = 10_000;
  const deadline = Date.now() + timeoutMs;

  while (Date.now() < deadline) {
    const run = await ghFetch<WorkflowRun>(
      `/repos/${REPO}/actions/runs/${runId}`
    );
    if (run.status === "completed") {
      if (run.conclusion !== "success") {
        throw new Error(
          `Run ${runId} selesai dengan conclusion: ${run.conclusion}`
        );
      }
      return run;
    }
    await new Promise((r) => setTimeout(r, interval));
  }
  throw new Error(`Timeout menunggu run ${runId}.`);
}

// --- version helpers ---

function bumpVersion(version: string, type: "patch" | "minor" | "major"): string {
  const [maj, min, pat] = version.split(".").map(Number);
  if (type === "major") return `${maj + 1}.0.0`;
  if (type === "minor") return `${maj}.${min + 1}.0`;
  return `${maj}.${min}.${pat + 1}`;
}

function readPkgVersion(): string {
  const pkg = JSON.parse(readFileSync(path.join(PROJECT_ROOT, "package.json"), "utf-8"));
  return pkg.version as string;
}

function applyVersionBump(newVersion: string): void {
  const pkgPath = path.join(PROJECT_ROOT, "package.json");
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
  pkg.version = newVersion;
  writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
}

// --- deployed version check ---

async function waitForDeployedVersion(expected: string, timeoutMs = 5 * 60 * 1000): Promise<string> {
  if (!BASE_URL) return "BASE_URL tidak di-set, skip cek versi stg.";

  const url = `${BASE_URL}${VERSION_PATH}`;
  const interval = 15_000;
  const maxAttempts = Math.ceil(timeoutMs / interval);
  let last = "";

  for (let i = 1; i <= maxAttempts; i++) {
    await new Promise((r) => setTimeout(r, interval));
    try {
      const res = await fetch(url);
      const data = (await res.json()) as { version?: string };
      last = data.version ?? "?";
      if (last === expected) {
        return `Versi terverifikasi di stg: ${last}`;
      }
    } catch {
      last = "error fetch";
    }
  }
  return `Timeout: versi stg masih ${last}, expected ${expected}`;
}

// --- MCP server ---

const server = new Server(
  { name: "deploy-stg", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "deploy",
      description:
        "Full deploy ke stg: bump version, commit, push ke build remote, publish Docker image, tunggu selesai, repull Portainer, verifikasi versi.",
      inputSchema: {
        type: "object",
        properties: {
          stack_name: {
            type: "string",
            description: "Nama stack Portainer. Jika tidak diisi, pakai env STACK_NAME.",
          },
          bump: {
            type: "string",
            enum: ["patch", "minor", "major"],
            description: "Jenis bump versi (default: patch)",
            default: "patch",
          },
        },
        required: [],
      },
    },
    {
      name: "publish",
      description:
        "Trigger workflow publish.yml: build & push Docker image ke GHCR (selalu stg, tag dari package.json). Kembalikan URL run.",
      inputSchema: { type: "object", properties: {}, required: [] },
    },
    {
      name: "repull",
      description:
        "Trigger workflow re-pull.yml: redeploy stack di Portainer stg dengan pull image terbaru. Kembalikan URL run.",
      inputSchema: {
        type: "object",
        properties: {
          stack_name: {
            type: "string",
            description: "Nama stack Portainer. Jika tidak diisi, pakai env STACK_NAME.",
          },
        },
        required: [],
      },
    },
    {
      name: "run_status",
      description:
        "Cek status GitHub Actions run terbaru untuk workflow tertentu, atau semua workflow.",
      inputSchema: {
        type: "object",
        properties: {
          workflow: {
            type: "string",
            enum: ["publish.yml", "re-pull.yml", "all"],
            description: "Nama workflow file atau 'all' untuk semua (default: all)",
            default: "all",
          },
          limit: {
            type: "number",
            description: "Jumlah run yang ditampilkan (default 5)",
            default: 5,
          },
        },
        required: [],
      },
    },
    {
      name: "check_version",
      description:
        "Bandingkan versi lokal (package.json) dengan versi yang berjalan di stg (/api/version-app).",
      inputSchema: { type: "object", properties: {}, required: [] },
    },
  ],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  try {
    // ── deploy ─────────────────────────────────────────────────────────────
    if (name === "deploy") {
      const { stack_name: _sn, bump = "patch" } = (args ?? {}) as {
        stack_name?: string;
        bump?: "patch" | "minor" | "major";
      };
      const stack_name = _sn || DEFAULT_STACK_NAME;
      if (!stack_name) throw new Error("stack_name tidak diisi dan env STACK_NAME kosong.");

      // 0. Cek migrasi — buat otomatis jika schema ada perubahan
      let migrationCreated = false;
      try {
        execFileSync(
          "./node_modules/.bin/prisma",
          ["migrate", "diff", "--from-migrations", "prisma/migrations", "--to-schema-datamodel", "prisma/schema.prisma", "--exit-code"],
          { encoding: "utf-8", cwd: PROJECT_ROOT, stdio: "pipe" }
        );
      } catch {
        // Ada schema diff — buat migration otomatis
        execFileSync(
          "./node_modules/.bin/prisma",
          ["migrate", "dev", "--create-only", "--name", "auto"],
          { encoding: "utf-8", cwd: PROJECT_ROOT, stdio: "pipe" }
        );
        migrationCreated = true;
      }

      const oldVersion = readPkgVersion();
      const newVersion = bumpVersion(oldVersion, bump);

      // 1. Bump version in package.json
      applyVersionBump(newVersion);

      // 2. Commit (version bump + migration jika ada)
      GIT(["add", "package.json", "prisma/migrations"]);
      GIT(["commit", "-m", migrationCreated
        ? `bump: version ${newVersion} + migration`
        : `bump: version ${newVersion}`
      ]);

      // 3. Push to build remote (GitHub)
      const currentBranch = GIT(["rev-parse", "--abbrev-ref", "HEAD"]);
      GIT(["push", "build", `${currentBranch}:stg`, "--force"]);

      // 4. Trigger publish workflow
      await triggerWorkflow("publish.yml", STACK_ENV, {
        stack_env: STACK_ENV,
        tag: newVersion,
      });
      await new Promise((r) => setTimeout(r, 4000));

      const publishRun = await getLatestRun("publish.yml");

      // 5. Wait for publish to finish
      await waitForRun(publishRun.id);

      // 6. Trigger repull
      await triggerWorkflow("re-pull.yml", STACK_ENV, {
        stack_name,
        stack_env: STACK_ENV,
      });
      await new Promise((r) => setTimeout(r, 4000));

      const repullRun = await getLatestRun("re-pull.yml");

      // 7. Wait for repull, then verify version
      await new Promise((r) => setTimeout(r, 30_000));
      const versionCheck = await waitForDeployedVersion(newVersion);

      const localVer = readPkgVersion();

      return {
        content: [
          {
            type: "text",
            text: [
              `Deploy selesai: ${stack_name}-${STACK_ENV} @ ${newVersion} (dari ${oldVersion})`,
              `Publish run  : ${publishRun.html_url}`,
              `Repull run   : ${repullRun.html_url}`,
              ``,
              `Versi lokal  : ${localVer}`,
              versionCheck,
            ].join("\n"),
          },
        ],
      };
    }

    // ── publish ────────────────────────────────────────────────────────────
    if (name === "publish") {
      const tag = readPkgVersion();

      await triggerWorkflow("publish.yml", STACK_ENV, {
        stack_env: STACK_ENV,
        tag,
      });
      await new Promise((r) => setTimeout(r, 3000));

      const run = await getLatestRun("publish.yml");

      return {
        content: [{ type: "text", text: `Publish triggered: ${STACK_ENV}-${tag}\nRun: ${run.html_url}` }],
      };
    }

    // ── repull ─────────────────────────────────────────────────────────────
    if (name === "repull") {
      const { stack_name: _sn } = (args ?? {}) as { stack_name?: string };
      const stack_name = _sn || DEFAULT_STACK_NAME;
      if (!stack_name) throw new Error("stack_name tidak diisi dan env STACK_NAME kosong.");

      await triggerWorkflow("re-pull.yml", STACK_ENV, {
        stack_name,
        stack_env: STACK_ENV,
      });
      await new Promise((r) => setTimeout(r, 3000));

      const run = await getLatestRun("re-pull.yml");

      return {
        content: [{ type: "text", text: `Repull triggered: ${stack_name}-${STACK_ENV}\nRun: ${run.html_url}` }],
      };
    }

    // ── run_status ─────────────────────────────────────────────────────────
    if (name === "run_status") {
      const { workflow = "all", limit = 5 } = (args ?? {}) as {
        workflow?: string;
        limit?: number;
      };

      const runs = await listRuns(workflow, limit);
      const output = runs
        .map(
          (r) =>
            `[${r.status}/${r.conclusion ?? "-"}] ${r.name} — ${r.run_started_at}\n  ${r.html_url}`
        )
        .join("\n");

      return {
        content: [{ type: "text", text: output || "Tidak ada run ditemukan." }],
      };
    }

    // ── check_version ──────────────────────────────────────────────────────
    if (name === "check_version") {
      const localVersion = readPkgVersion();
      let stgVersion = "tidak dapat dijangkau";

      if (BASE_URL) {
        try {
          const res = await fetch(`${BASE_URL}${VERSION_PATH}`);
          const data = (await res.json()) as { version?: string };
          stgVersion = data.version ?? "?";
        } catch (e) {
          stgVersion = `error: ${(e as Error).message}`;
        }
      } else {
        stgVersion = "BASE_URL tidak di-set";
      }

      const match = localVersion === stgVersion ? "✓ sama" : "✗ beda";

      return {
        content: [
          {
            type: "text",
            text: [
              `Lokal (package.json) : ${localVersion}`,
              `Stg (${VERSION_PATH}): ${stgVersion}`,
              `Status               : ${match}`,
            ].join("\n"),
          },
        ],
      };
    }

    return {
      content: [{ type: "text", text: `Unknown tool: ${name}` }],
      isError: true,
    };
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
  }
});

const transport = new StdioServerTransport();
await server.connect(transport);
