import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

/**
 * The convergence engine: one checklist of provisioning checks, each with a
 * `detect` (what is the current state?) and, where a machine can act, a
 * `repair` (bring it to the ok state). Three commands are thin views over it:
 *
 *   init --run     converge(repair) from zero
 *   doctor --deep  converge(detect) — report only
 *   doctor --fix   converge(repair) — repair what detect flags
 *
 * Checks without `detect` are manual dashboard steps; checks without `repair`
 * can only be reported. Repair mode never aborts mid-list: every check runs,
 * failures are collected, callers decide whether to throw.
 */

/** Repo/package root — wrangler always runs from <root>/worker regardless of CWD. */
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
export const WORKER_DIR: string = join(ROOT, "worker");

export interface ExecResult {
  code: number;
  out: string;
}

/** Everything a check needs to detect or repair provisioning state. Injectable for tests. */
export interface ConvergeDeps {
  /** Run a command from worker/ with optional stdin; returns exit code + combined output. */
  exec(cmd: string, args: string[], stdin?: string): Promise<ExecResult>;
  wranglerCmd: string;
  /** Read worker/wrangler.jsonc. */
  readConfig(): Promise<string>;
  /** Write a database id into worker/wrangler.jsonc. */
  patchD1Id(databaseId: string): Promise<void>;
  /** Whether the worker bundle's node_modules are installed. */
  hasNodeModules(): Promise<boolean>;
  /** GET <url>/health; null when unreachable/non-200. */
  fetchHealth(url: string): Promise<{ send: boolean } | null>;
  /** API key the worker secret converges to (generated by init, or the profile's). */
  apiKey: string;
  /** Deployed worker URL when known — enables deploy detection. */
  workerUrl?: string | undefined;
}

export interface CheckState {
  ok: boolean;
  detail: string;
}

export interface ConvergeCheck {
  title: string;
  /** The manual command, for dry-plan display. */
  cmd?: string;
  note?: string;
  detect?: (d: ConvergeDeps) => Promise<CheckState>;
  repair?: (d: ConvergeDeps) => Promise<string>;
}

export interface ConvergeResult {
  title: string;
  ok: boolean;
  detail: string;
  fixed: boolean;
  /** True when only a human can verify/perform this step. */
  manual: boolean;
}

/** Wrangler (or bun) invocations shared by init and doctor. */
export interface ProvisionDeps {
  detectWrangler(): Promise<{ cmd: string; version: string } | null>;
  whoami(cmd: string): Promise<boolean>;
  convergeDeps(wranglerCmd: string): ConvergeDeps;
}

const msg = (e: unknown): string => (e instanceof Error ? e.message : String(e));

function failOutput(res: ExecResult): string {
  return res.out.trim() || `exit ${res.code}`;
}

// ---- D1 helpers ----

function parseJsonArray<T>(out: string): T[] {
  const arr = out.match(/\[[\s\S]*\]/);
  try {
    return arr ? (JSON.parse(arr[0]) as T[]) : [];
  } catch {
    return [];
  }
}

async function findDatabaseId(d: ConvergeDeps): Promise<string | null> {
  const list = await d.exec(d.wranglerCmd, ["d1", "list", "--json"]);
  if (list.code !== 0) return null;
  return (
    parseJsonArray<{ name?: string; uuid?: string }>(list.out).find((x) => x.name === "cloudmail")
      ?.uuid ?? null
  );
}

/** Probe the remote emails table for the direction column. */
async function probeSchema(
  d: ConvergeDeps,
): Promise<"current" | "pre-send" | { failed: string }> {
  const res = await d.exec(d.wranglerCmd, [
    "d1",
    "execute",
    "cloudmail",
    "--remote",
    "--json",
    "--command",
    "SELECT name FROM pragma_table_info('emails') WHERE name = 'direction'",
  ]);
  if (res.code !== 0) {
    return { failed: res.out.trim().split("\n")[0]?.slice(0, 120) || `exit ${res.code}` };
  }
  const rows = parseJsonArray<{ results?: Array<{ name?: string }> }>(res.out);
  const found = rows.some((r) => r.results?.some((row) => row.name === "direction"));
  return found ? "current" : "pre-send";
}

// ---- The checklist ----

/**
 * Ordered provisioning checklist. `wrangler` is the resolved command used for
 * display strings; `domain` gates the Email Routing/Sending repairs.
 */
export function buildChecklist(wrangler: string, domain?: string): ConvergeCheck[] {
  const checks: ConvergeCheck[] = [
    {
      title: "Create D1 database",
      cmd: `${wrangler} d1 create cloudmail`,
      detect: async (d) =>
        (await findDatabaseId(d))
          ? { ok: true, detail: "cloudmail database exists" }
          : { ok: false, detail: "no cloudmail D1 database on this account" },
      repair: async (d) => {
        const create = await d.exec(d.wranglerCmd, ["d1", "create", "cloudmail"]);
        if (create.code !== 0 && !/already exists/i.test(create.out)) {
          throw new Error(failOutput(create));
        }
        return "database created";
      },
    },
    {
      title: "Wire D1 database_id into wrangler.jsonc",
      note: "Automatic with --run/--fix; otherwise copy the database_id into worker/wrangler.jsonc.",
      detect: async (d) =>
        /"database_id"\s*:\s*"[0-9a-fA-F-]{36}"/.test(await d.readConfig())
          ? { ok: true, detail: "database_id wired" }
          : { ok: false, detail: "database_id placeholder not replaced" },
      repair: async (d) => {
        const id = await findDatabaseId(d);
        if (!id) throw new Error("could not resolve the cloudmail database id from `d1 list`");
        await d.patchD1Id(id);
        return `database_id ${id} wired`;
      },
    },
    {
      title: "Apply D1 schema",
      cmd: `${wrangler} d1 execute cloudmail --remote --file schema.sql`,
      detect: async (d) => {
        const state = await probeSchema(d);
        if (state === "current") return { ok: true, detail: "schema current (direction column present)" };
        if (state === "pre-send") {
          return { ok: false, detail: "pre-send schema: direction column missing (migration 0002)" };
        }
        return { ok: false, detail: `schema probe failed: ${state.failed}` };
      },
      repair: async (d) => {
        const schema = await d.exec(d.wranglerCmd, [
          "d1", "execute", "cloudmail", "--remote", "--file", "schema.sql",
        ]);
        if (schema.code !== 0) throw new Error(failOutput(schema));
        if ((await probeSchema(d)) === "pre-send") {
          const migrate = await d.exec(d.wranglerCmd, [
            "d1", "execute", "cloudmail", "--remote", "--file", "migrations/0002-direction.sql",
          ]);
          if (migrate.code !== 0) throw new Error(failOutput(migrate));
          return "schema applied + direction migration run";
        }
        return "schema applied";
      },
    },
    {
      title: "Set worker API key",
      cmd: `${wrangler} secret put API_KEY`,
      note: "init uses its generated apiKey; doctor --fix converges to the profile's apiKey.",
      detect: async (d) => {
        const res = await d.exec(d.wranglerCmd, ["secret", "list"]);
        if (res.code !== 0) {
          return { ok: false, detail: "secret list failed (worker not deployed yet?)" };
        }
        return /"API_KEY"/.test(res.out)
          ? { ok: true, detail: "API_KEY secret set" }
          : { ok: false, detail: "API_KEY secret missing" };
      },
      repair: async (d) => {
        if (!d.apiKey) {
          throw new Error("no apiKey to converge to — run `cloudmail init` or `cloudmail config set`");
        }
        const res = await d.exec(d.wranglerCmd, ["secret", "put", "API_KEY"], d.apiKey);
        if (res.code !== 0) throw new Error(failOutput(res));
        return "API_KEY secret set";
      },
    },
    {
      title: "Install dependencies",
      cmd: "bun install",
      note: "The worker bundle needs node_modules (postal-mime); the CLI itself does not.",
      detect: async (d) =>
        (await d.hasNodeModules())
          ? { ok: true, detail: "node_modules present" }
          : { ok: false, detail: "worker dependencies not installed" },
      repair: async (d) => {
        const res = await d.exec("bun", ["install"]);
        if (res.code !== 0) throw new Error(failOutput(res));
        return "dependencies installed";
      },
    },
    {
      title: "Deploy worker",
      cmd: `${wrangler} deploy`,
      detect: async (d) => {
        if (!d.workerUrl) return { ok: false, detail: "no workerUrl to probe (no profile yet)" };
        const health = await d.fetchHealth(d.workerUrl);
        return health
          ? { ok: true, detail: `GET /health ok (send: ${health.send})` }
          : { ok: false, detail: `health probe failed at ${d.workerUrl}` };
      },
      repair: async (d) => {
        const res = await d.exec(d.wranglerCmd, ["deploy"]);
        if (res.code !== 0) throw new Error(failOutput(res));
        return "worker deployed";
      },
    },
  ];

  if (domain) {
    checks.push(
      {
        title: "Enable Email Routing",
        cmd: `${wrangler} email routing enable ${domain}`,
        repair: async (d) => {
          const res = await d.exec(d.wranglerCmd, ["email", "routing", "enable", domain]);
          if (res.code !== 0) throw new Error(failOutput(res));
          return `Email Routing enabled for ${domain}`;
        },
      },
      {
        title: "Enable Email Sending",
        cmd: `${wrangler} email sending enable ${domain}`,
        note: "Open beta; if the command is unauthorized, onboard the domain in the dashboard (Email Service → Email Sending).",
        repair: async (d) => {
          const res = await d.exec(d.wranglerCmd, ["email", "sending", "enable", domain]);
          if (res.code !== 0) throw new Error(failOutput(res));
          return `Email Sending enabled for ${domain}`;
        },
      },
    );
  } else {
    checks.push(
      {
        title: "Enable Email Routing",
        note: "Manual: rerun with --domain <domain>, or enable Email Routing for your domain.",
      },
      {
        title: "Enable Email Sending",
        note: "Manual: rerun with --domain <domain>, or enable Email Sending for your domain.",
      },
    );
  }
  checks.push({
    title: "Route address to worker",
    note: "Dashboard: Email Routing → Routing rules — send agent@<domain> (or the catch-all) to the cloudmail Worker.",
  });
  return checks;
}

// ---- The engine ----

/**
 * Run the checklist. Detect mode reports state; repair mode additionally runs
 * `repair` for every check that is not already ok. Never throws — failures are
 * collected per check and every check runs.
 */
export async function converge(
  checks: ConvergeCheck[],
  deps: ConvergeDeps,
  repairMode: boolean,
): Promise<ConvergeResult[]> {
  const results: ConvergeResult[] = [];
  for (const check of checks) {
    let state: CheckState | null = null;
    if (check.detect) {
      try {
        state = await check.detect(deps);
      } catch (e) {
        state = { ok: false, detail: msg(e) };
      }
    }

    const manualDetail = `manual/unverified — ${check.note ?? check.cmd ?? check.title}`;
    if (!repairMode || state?.ok) {
      results.push({
        title: check.title,
        ok: state?.ok ?? true,
        detail: state?.detail ?? manualDetail,
        fixed: false,
        manual: !check.detect,
      });
      continue;
    }
    if (!check.repair) {
      results.push({
        title: check.title,
        ok: state ? state.ok : true,
        detail: state?.detail ?? manualDetail,
        fixed: false,
        manual: true,
      });
      continue;
    }
    try {
      const detail = await check.repair(deps);
      results.push({ title: check.title, ok: true, detail, fixed: true, manual: false });
    } catch (e) {
      results.push({ title: check.title, ok: false, detail: msg(e), fixed: false, manual: false });
    }
  }
  return results;
}

// ---- Default (Bun) dependencies ----

async function spawn(cmd: string, args: string[], input?: string): Promise<ExecResult> {
  const words = cmd.split(/\s+/).filter(Boolean);
  const proc = Bun.spawn([...words, ...args], {
    cwd: WORKER_DIR,
    stdin: input === undefined ? "ignore" : "pipe",
    stdout: "pipe",
    stderr: "pipe",
  });
  if (input !== undefined && proc.stdin) {
    proc.stdin.write(`${input}\n`);
    proc.stdin.end();
  }
  const [code, stdout, stderr] = await Promise.all([
    proc.exited,
    new Response(proc.stdout).text(),
    new Response(proc.stderr).text(),
  ]);
  return { code, out: `${stdout}${stderr}` };
}

async function patchD1IdOnDisk(databaseId: string): Promise<void> {
  const path = join(WORKER_DIR, "wrangler.jsonc");
  const source = await Bun.file(path).text();
  const placeholder = '"database_id": "REPLACE_WITH_YOUR_D1_ID"';
  let patched = source.replace(placeholder, `"database_id": "${databaseId}"`);
  if (patched === source) {
    patched = source.replace(
      /("database_name"\s*:\s*"cloudmail"[\s\S]*?"database_id"\s*:\s*")[^"]*(")/,
      `$1${databaseId}$2`,
    );
  }
  if (patched === source) {
    throw new Error("cloudmail D1 database entry was not found");
  }
  await Bun.write(path, patched);
}

/** Bun-backed dependencies shared by `init` and `doctor --deep/--fix`. */
export function defaultProvisionDeps(opts: { apiKey: string; workerUrl?: string }): ProvisionDeps {
  return {
    async detectWrangler() {
      for (const cmd of ["wrangler", "npx --yes wrangler"]) {
        try {
          const result = await spawn(cmd, ["--version"]);
          if (result.code === 0) return { cmd, version: result.out.trim() };
        } catch {
          // Try the fallback command.
        }
      }
      return null;
    },
    async whoami(cmd) {
      try {
        return (await spawn(cmd, ["whoami"])).code === 0;
      } catch {
        return false;
      }
    },
    convergeDeps(wranglerCmd) {
      return {
        wranglerCmd,
        apiKey: opts.apiKey,
        workerUrl: opts.workerUrl,
        exec: spawn,
        readConfig: () => Bun.file(join(WORKER_DIR, "wrangler.jsonc")).text(),
        patchD1Id: patchD1IdOnDisk,
        hasNodeModules: () => Bun.file(join(ROOT, "node_modules", "postal-mime", "package.json")).exists(),
        async fetchHealth(url) {
          try {
            const res = await fetch(`${url.replace(/\/+$/, "")}/health`, {
              signal: AbortSignal.timeout(10_000),
            });
            if (!res.ok) return null;
            const body = (await res.json()) as { send?: unknown };
            return { send: body?.send === true };
          } catch {
            return null;
          }
        },
      };
    },
  };
}
