#!/usr/bin/env node
/**
 * MCP Server: GitHub Actions Workflow Tools + Deploy-STG Pre/Post Steps
 * Tools: trigger_publish, trigger_repull, get_workflow_runs, watch_workflow_run,
 *        check_migrations, create_migration, bump_version, commit_and_push_stg, check_stg_version
 */

import { spawnSync } from "child_process";
import { readFileSync, writeFileSync } from "fs";
import { join } from "path";

const REPO = "bipprojectbali/desa-darmasaba";
const CWD = process.cwd();

// --- MCP Protocol Helpers ---

function send(obj) {
  process.stdout.write(JSON.stringify(obj) + "\n");
}

function respond(id, result) {
  send({ jsonrpc: "2.0", id, result });
}

function respondError(id, code, message) {
  send({ jsonrpc: "2.0", id, error: { code, message } });
}

// --- Shell Helper ---

function runCmd(cmd, opts = {}) {
  const r = spawnSync("sh", ["-c", cmd], {
    encoding: "utf-8",
    cwd: CWD,
    ...opts,
  });
  if (r.error) return { ok: false, out: r.error.message };
  if (r.status !== 0) return { ok: false, out: (r.stderr || r.stdout || "").trim() };
  return { ok: true, out: (r.stdout || "").trim() };
}

function getLatestRunId(workflow, delaySecs = 3) {
  const r = runCmd(
    `sleep ${delaySecs} && gh run list --workflow=${workflow} --repo ${REPO} --limit 1 --json databaseId -q '.[0].databaseId'`
  );
  return r.ok ? r.out.trim() : null;
}

// --- Tool Definitions ---

const TOOLS = [
  {
    name: "check_migrations",
    description: "Cek status Prisma migrations. Returns apakah ada pending migrations.",
    inputSchema: { type: "object", properties: {} },
  },
  {
    name: "create_migration",
    description: "Buat Prisma migration baru (prisma migrate dev). Jalankan hanya jika check_migrations mendeteksi pending/drift.",
    inputSchema: {
      type: "object",
      properties: {
        name: {
          type: "string",
          description: "Nama migration (e.g. bump-stg-0.1.26)",
        },
      },
      required: ["name"],
    },
  },
  {
    name: "bump_version",
    description: "Baca versi dari package.json, increment patch (+1), tulis kembali. Returns old_version dan new_version.",
    inputSchema: { type: "object", properties: {} },
  },
  {
    name: "commit_and_push_stg",
    description: "Stage package.json + prisma/migrations, commit, lalu push ke branch stg menggunakan GH_TOKEN.",
    inputSchema: {
      type: "object",
      properties: {
        message: {
          type: "string",
          description: "Commit message. Jika tidak diisi, auto-generate dari versi package.json.",
        },
      },
    },
  },
  {
    name: "check_stg_version",
    description: "Bandingkan versi lokal (package.json) dengan versi di STG (/api/version). Tunggu container siap jika perlu.",
    inputSchema: {
      type: "object",
      properties: {
        wait_seconds: {
          type: "number",
          description: "Detik tunggu sebelum fetch STG (default: 30, untuk memberi waktu container siap).",
        },
      },
    },
  },
  {
    name: "trigger_publish",
    description:
      "Trigger publish.yml workflow: build & push Docker image ke GHCR. Returns run ID.",
    inputSchema: {
      type: "object",
      properties: {
        stack_env: {
          type: "string",
          enum: ["dev", "stg", "prod"],
          description: "Target environment. Default: stg.",
        },
        tag: {
          type: "string",
          description: "Image tag — harus sama persis dengan versi di package.json (e.g. 0.1.25).",
        },
        stack_name: {
          type: "string",
          description: `Nama stack. Default: nilai env STACK_NAME (${process.env.STACK_NAME || "belum diset"}).`,
        },
      },
      required: ["stack_env", "tag"],
    },
  },
  {
    name: "trigger_repull",
    description:
      "Trigger re-pull.yml workflow: redeploy stack di Portainer. Jalankan HANYA setelah publish berhasil.",
    inputSchema: {
      type: "object",
      properties: {
        stack_name: {
          type: "string",
          description: `Nama stack (e.g. desa-darmasaba). Stack yang di-deploy: <stack_name>-<stack_env>. Default: nilai env STACK_NAME (${process.env.STACK_NAME || "belum diset"}).`,
        },
        stack_env: {
          type: "string",
          enum: ["dev", "stg", "prod"],
          description: "Target environment.",
        },
      },
      required: ["stack_env"],
    },
  },
  {
    name: "get_workflow_runs",
    description: "List run terbaru dari suatu workflow beserta status dan conclusion-nya.",
    inputSchema: {
      type: "object",
      properties: {
        workflow: {
          type: "string",
          description: "Nama file workflow (e.g. publish.yml, re-pull.yml)",
        },
        limit: {
          type: "number",
          description: "Jumlah run yang ditampilkan (default: 5)",
        },
      },
      required: ["workflow"],
    },
  },
  {
    name: "watch_workflow_run",
    description:
      "Lihat status detail sebuah workflow run: apakah in_progress, success, atau failure.",
    inputSchema: {
      type: "object",
      properties: {
        run_id: {
          type: "number",
          description: "ID workflow run (dapat dari trigger_publish / trigger_repull / get_workflow_runs)",
        },
      },
      required: ["run_id"],
    },
  },
];

// --- Tool Handlers ---

function handleCheckMigrations() {
  const r = runCmd("bunx prisma migrate status 2>&1", { timeout: 30000 });
  const out = r.out || "";
  const hasPending =
    out.includes("following migration(s) have not yet been applied") ||
    out.includes("drift") ||
    out.includes("pending");
  const isUpToDate =
    out.includes("Database schema is up to date") ||
    out.includes("up to date");
  return {
    ok: true,
    needs_migration: hasPending && !isUpToDate,
    is_up_to_date: isUpToDate,
    output: out,
  };
}

function handleCreateMigration(args) {
  const { name } = args;
  const r = runCmd(
    `echo "" | bunx prisma migrate dev --name ${name} --skip-generate 2>&1`,
    { timeout: 120000 }
  );
  return {
    ok: r.ok,
    output: r.out,
    error: r.ok ? undefined : r.out,
  };
}

function handleBumpVersion() {
  const pkgPath = join(CWD, "package.json");
  let pkg;
  try {
    pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
  } catch (e) {
    return { ok: false, error: `Gagal baca package.json: ${e.message}` };
  }
  const oldVersion = pkg.version;
  const [maj, min, pat] = oldVersion.split(".").map(Number);
  const newVersion = `${maj}.${min}.${pat + 1}`;
  pkg.version = newVersion;
  try {
    writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
  } catch (e) {
    return { ok: false, error: `Gagal tulis package.json: ${e.message}` };
  }
  return { ok: true, old_version: oldVersion, new_version: newVersion };
}

function handleCommitAndPushStg(args) {
  const { message } = args || {};

  // Baca versi untuk auto commit message
  let version = "unknown";
  try {
    const pkg = JSON.parse(readFileSync(join(CWD, "package.json"), "utf-8"));
    version = pkg.version;
  } catch {}

  const commitMsg = message || `chore: bump version to ${version} for stg deploy`;

  // Stage file
  runCmd("git add package.json 2>&1");
  runCmd("git add prisma/migrations/ 2>&1");

  // Cek apakah ada yang di-stage
  const statusR = runCmd("git diff --cached --name-only 2>&1");
  if (!statusR.out.trim()) {
    return { ok: false, error: "Tidak ada perubahan yang di-stage untuk di-commit." };
  }

  // Commit
  const commitR = runCmd(`git commit -m "${commitMsg}" 2>&1`);
  if (!commitR.ok) return { ok: false, step: "commit", error: commitR.out };

  // Push ke stg menggunakan GH_TOKEN jika tersedia
  const token = process.env.GH_TOKEN;
  const pushCmd = token
    ? `git push https://x-access-token:${token}@github.com/${REPO}.git HEAD:stg 2>&1`
    : `git push origin HEAD:stg 2>&1`;
  const pushR = runCmd(pushCmd, { timeout: 60000 });
  if (!pushR.ok) return { ok: false, step: "push", error: pushR.out };

  return {
    ok: true,
    message: `Berhasil commit "${commitMsg}" dan push ke stg`,
    version,
  };
}

function handleCheckStgVersion(args) {
  const waitSecs = (args && args.wait_seconds) || 30;
  const baseUrl = process.env.BASE_URL;
  if (!baseUrl) {
    return { ok: false, error: "BASE_URL tidak ada di environment — isi di .env" };
  }

  // Baca versi lokal
  let localVersion;
  try {
    const pkg = JSON.parse(readFileSync(join(CWD, "package.json"), "utf-8"));
    localVersion = pkg.version;
  } catch (e) {
    return { ok: false, error: `Gagal baca package.json: ${e.message}` };
  }

  // Tunggu container siap
  if (waitSecs > 0) {
    runCmd(`sleep ${waitSecs}`);
  }

  // Fetch versi dari STG
  const r = runCmd(`curl -sf --max-time 10 "${baseUrl}/api/version" 2>&1`);
  if (!r.ok) {
    return {
      ok: false,
      error: `Gagal fetch ${baseUrl}/api/version: ${r.out}`,
      local_version: localVersion,
    };
  }

  let stgVersion;
  try {
    stgVersion = JSON.parse(r.out).version;
  } catch {
    return {
      ok: false,
      error: `Gagal parse response: ${r.out}`,
      local_version: localVersion,
    };
  }

  const match = localVersion === stgVersion;
  return {
    ok: true,
    local_version: localVersion,
    stg_version: stgVersion,
    match,
    status: match
      ? "✓ DEPLOY BERHASIL — versi STG sudah sesuai"
      : "✗ VERSI BERBEDA — cek container logs di Portainer",
  };
}

function handleTriggerPublish(args) {
  const { stack_env = "stg", tag, stack_name } = args;
  const resolvedStackName = stack_name || process.env.STACK_NAME;
  let triggerCmd = `gh workflow run publish.yml --repo ${REPO} --ref ${stack_env} -f stack_env=${stack_env} -f tag=${tag}`;
  if (resolvedStackName) triggerCmd += ` -f stack_name=${resolvedStackName}`;
  const r = runCmd(triggerCmd);
  if (!r.ok) return { ok: false, error: r.out };

  const runId = getLatestRunId("publish.yml");
  return {
    ok: true,
    message: `Workflow publish.yml di-trigger (env=${stack_env}, tag=${tag}${resolvedStackName ? `, stack=${resolvedStackName}` : ""})`,
    run_id: runId ? Number(runId) : null,
    monitor_hint: runId
      ? `Gunakan watch_workflow_run dengan run_id: ${runId}`
      : "Gunakan get_workflow_runs untuk mendapatkan run_id terbaru",
  };
}

function handleTriggerRepull(args) {
  const resolvedStackName = args.stack_name || process.env.STACK_NAME;
  const { stack_env } = args;
  if (!resolvedStackName) {
    return { ok: false, error: "stack_name wajib diisi atau set env STACK_NAME di .mcp.json" };
  }
  const stack_name = resolvedStackName;
  const triggerCmd = `gh workflow run re-pull.yml --repo ${REPO} --ref main -f stack_name=${stack_name} -f stack_env=${stack_env}`;
  const r = runCmd(triggerCmd);
  if (!r.ok) return { ok: false, error: r.out };

  const runId = getLatestRunId("re-pull.yml");
  return {
    ok: true,
    message: `Workflow re-pull.yml di-trigger (stack=${stack_name}-${stack_env})`,
    run_id: runId ? Number(runId) : null,
    monitor_hint: runId
      ? `Gunakan watch_workflow_run dengan run_id: ${runId}`
      : "Gunakan get_workflow_runs untuk mendapatkan run_id terbaru",
  };
}

function handleGetWorkflowRuns(args) {
  const limit = args.limit || 5;
  const r = runCmd(
    `gh run list --workflow=${args.workflow} --repo ${REPO} --limit ${limit} --json databaseId,status,conclusion,displayTitle,createdAt,headBranch`
  );
  if (!r.ok) return { ok: false, error: r.out };
  try {
    return { ok: true, runs: JSON.parse(r.out) };
  } catch {
    return { ok: false, error: "Gagal parse output", raw: r.out };
  }
}

function handleWatchWorkflowRun(args) {
  const r = runCmd(
    `gh run view ${args.run_id} --repo ${REPO} --json status,conclusion,displayTitle,headBranch,createdAt,updatedAt,jobs`
  );
  if (!r.ok) return { ok: false, error: r.out };
  try {
    const data = JSON.parse(r.out);
    return {
      ok: true,
      run_id: args.run_id,
      status: data.status,
      conclusion: data.conclusion,
      title: data.displayTitle,
      branch: data.headBranch,
      jobs: (data.jobs || []).map((j) => ({
        name: j.name,
        status: j.status,
        conclusion: j.conclusion,
      })),
    };
  } catch {
    return { ok: false, error: "Gagal parse output", raw: r.out };
  }
}

// --- Request Router ---

function handleMessage(msg) {
  const { id, method, params } = msg;

  if (method === "initialize") {
    respond(id, {
      protocolVersion: "2024-11-05",
      capabilities: { tools: {} },
      serverInfo: { name: "deploy-stg", version: "2.0.0" },
    });
    return;
  }

  if (method === "notifications/initialized") return;

  if (method === "ping") {
    respond(id, {});
    return;
  }

  if (method === "tools/list") {
    respond(id, { tools: TOOLS });
    return;
  }

  if (method === "tools/call") {
    const { name, arguments: args = {} } = params;
    let result;

    if (name === "check_migrations")       result = handleCheckMigrations();
    else if (name === "create_migration")  result = handleCreateMigration(args);
    else if (name === "bump_version")      result = handleBumpVersion();
    else if (name === "commit_and_push_stg") result = handleCommitAndPushStg(args);
    else if (name === "check_stg_version") result = handleCheckStgVersion(args);
    else if (name === "trigger_publish")   result = handleTriggerPublish(args);
    else if (name === "trigger_repull")    result = handleTriggerRepull(args);
    else if (name === "get_workflow_runs") result = handleGetWorkflowRuns(args);
    else if (name === "watch_workflow_run") result = handleWatchWorkflowRun(args);
    else {
      respondError(id, -32601, `Tool tidak ditemukan: ${name}`);
      return;
    }

    respond(id, {
      content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
    });
    return;
  }

  respondError(id, -32601, `Method tidak dikenal: ${method}`);
}

// --- Stdin Loop ---

let buf = "";
process.stdin.setEncoding("utf-8");
process.stdin.on("data", (chunk) => {
  buf += chunk;
  const lines = buf.split("\n");
  buf = lines.pop();
  for (const line of lines) {
    const trimmed = line.trim();
    if (!trimmed) continue;
    try {
      handleMessage(JSON.parse(trimmed));
    } catch {
      // ignore malformed JSON
    }
  }
});
