"use strict";

const fs = require("fs");
const path = require("path");

const {
  PACKAGE_ROOT,
  GLOBAL_CLAUDE_DIR,
  MIGRATIONS_STATE_FILE,
} = require("./constants");
const {
  log,
  logHeader,
  logWarning,
  logError,
  logVerbose,
} = require("./logging");
const { writeSettingsAtomic } = require("./fs-utils");
const { assertSafeForShellQuoting } = require("./settings");
// ─── Migrations ─────────────────────────────────────────────────────────────

const MIGRATIONS = [
  {
    id: "019",
    phase: "pre-copy",
    description:
      "Extract Repository Paths from project-constants.md to project-paths.local.md",
    run({ dryRun, globalDir = GLOBAL_CLAUDE_DIR }) {
      const registryPath = path.join(
        globalDir,
        "evolv-coder-kit",
        "registry.yaml",
      );
      if (!fs.existsSync(registryPath)) {
        return "skip: no registry found";
      }
      const registryContent = fs.readFileSync(registryPath, "utf-8");
      const projectDirs = [];
      for (const line of registryContent.split("\n")) {
        const match = line.match(/^\s*path:\s*(.+)$/);
        if (match) projectDirs.push(match[1].trim());
      }
      let applied = 0;
      for (const dir of projectDirs) {
        const result = migration001({ projectDir: dir, dryRun });
        if (result.status === "applied") applied++;
      }
      if (applied === 0) return "skip: no projects needed migration";
      return `applied: migrated ${applied} project(s)`;
    },
  },
  {
    id: "020",
    phase: "post-copy",
    description:
      "Rewrite tilde-based hook paths in global settings.json to absolute paths",
    run({ dryRun, globalDir = GLOBAL_CLAUDE_DIR }) {
      return migration020({ dryRun, globalDir });
    },
  },
];

function readMigrationState(stateFile = MIGRATIONS_STATE_FILE) {
  try {
    if (fs.existsSync(stateFile)) {
      const data = JSON.parse(fs.readFileSync(stateFile, "utf-8"));
      if (data && Array.isArray(data.appliedMigrations)) {
        return {
          schemaVersion: data.schemaVersion || 1,
          appliedMigrations: data.appliedMigrations.filter(
            (id) => typeof id === "string",
          ),
          lastRunVersion: data.lastRunVersion || null,
          lastRunAt: data.lastRunAt || null,
          migrationDetails: data.migrationDetails || {},
          firstInstalledAt: data.firstInstalledAt || null,
        };
      }
    }
  } catch (readErr) {
    // File exists but failed to parse — warn about corruption
    try {
      if (fs.existsSync(stateFile)) {
        logWarning(
          `Migration state file corrupted (${readErr.message}), starting fresh`,
        );
      }
    } catch {
      /* ignore secondary errors */
    }
  }
  return {
    schemaVersion: 1,
    appliedMigrations: [],
    lastRunVersion: null,
    lastRunAt: null,
    migrationDetails: {},
    firstInstalledAt: null,
  };
}

function writeMigrationState(state, stateFile = MIGRATIONS_STATE_FILE) {
  try {
    const dir = path.dirname(stateFile);
    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
    // Ensure schema version and firstInstalledAt are always persisted
    state.schemaVersion = state.schemaVersion || 1;
    if (!state.firstInstalledAt) {
      state.firstInstalledAt = new Date().toISOString();
    }
    const tmp = stateFile + ".tmp";
    fs.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
    fs.renameSync(tmp, stateFile);
    return true;
  } catch (err) {
    logError(`Could not save migration state: ${err.message}`);
    log(
      "  Migration progress may not be persisted; will re-run on next install.",
    );
    return false;
  }
}

function runMigrations(
  phase,
  { dryRun = false, globalDir = GLOBAL_CLAUDE_DIR } = {},
) {
  const stateFile = path.join(globalDir, "evolv-coder-kit", ".migrations");
  const state = readMigrationState(stateFile);
  const isFreshInstall =
    state.appliedMigrations.length === 0 && !state.lastRunVersion;

  // Detect version downgrade (warn but don't block)
  if (phase === "pre-copy" && state.lastRunVersion) {
    try {
      const pkg = require(path.join(PACKAGE_ROOT, "package.json"));
      const lastParts = state.lastRunVersion.split(".").map(Number);
      const currParts = pkg.version.split(".").map(Number);
      // Validate semver: must have 3 numeric parts, none NaN
      const validSemver =
        lastParts.length === 3 &&
        lastParts.every((n) => !isNaN(n)) &&
        currParts.length === 3 &&
        currParts.every((n) => !isNaN(n));
      if (!validSemver || lastParts[0] > currParts[0] + 1) {
        logVerbose(
          `Unrecognized previous version "${state.lastRunVersion}" in migration state — skipping downgrade check`,
        );
      } else {
        const isDowngrade =
          lastParts[0] > currParts[0] ||
          (lastParts[0] === currParts[0] && lastParts[1] > currParts[1]) ||
          (lastParts[0] === currParts[0] &&
            lastParts[1] === currParts[1] &&
            lastParts[2] > currParts[2]);
        if (isDowngrade) {
          logWarning(
            `Downgrade detected: ${state.lastRunVersion} → ${pkg.version}. Proceeding, but some features may be unavailable.`,
          );
        }
      }
    } catch {
      /* ignore version comparison errors */
    }
  }

  const pending = MIGRATIONS.filter(
    (m) => m.phase === phase && !state.appliedMigrations.includes(m.id),
  );

  const results = {
    ran: 0,
    failed: 0,
    skipped: 0,
    failedIds: [],
    skippedIds: [],
  };

  // Fresh-install bootstrapping: migrations fix upgrade paths, not fresh installs.
  // Mark all current migrations as applied without running them.
  if (isFreshInstall && pending.length > 0 && !dryRun) {
    const pkg = require(path.join(PACKAGE_ROOT, "package.json"));
    for (const m of pending) {
      state.appliedMigrations.push(m.id);
      state.migrationDetails[m.id] = {
        appliedAt: new Date().toISOString(),
        duration: 0,
        packageVersion: pkg.version,
        result: "skip: bootstrapped on fresh install",
        status: "skipped",
      };
      results.skipped++;
      results.skippedIds.push(m.id);
    }
    state.lastRunVersion = pkg.version;
    state.lastRunAt = new Date().toISOString();
    writeMigrationState(state, stateFile);
    return results;
  }

  if (pending.length === 0) {
    // Always write state file on pre-copy so .migrations exists after fresh install
    if (phase === "pre-copy" && !dryRun) {
      const pkg = require(path.join(PACKAGE_ROOT, "package.json"));
      state.lastRunVersion = pkg.version;
      state.lastRunAt = new Date().toISOString();
      writeMigrationState(state, stateFile);
    }
    return results;
  }

  logHeader(`Migrations (${phase})`);

  // Pre-migration batch backup
  if (!dryRun && fs.existsSync(stateFile)) {
    try {
      fs.copyFileSync(stateFile, stateFile + ".pre-migration.bak");
    } catch {
      /* backup is best-effort */
    }
  }

  for (const migration of pending) {
    try {
      const pkg = require(path.join(PACKAGE_ROOT, "package.json"));
      const start = Date.now();
      const result = migration.run({
        dryRun,
        globalDir,
        packageVersion: pkg.version,
      });
      const duration = Date.now() - start;
      log(`[${migration.id}] ${migration.description}`);
      log(`  ${result}`);

      // Parse return value prefix to determine status
      const resultStr = String(result || "");
      const isSkip = resultStr.startsWith("skip:");
      const isDryRun = resultStr.startsWith("dry-run:");

      if (isSkip) {
        results.skipped++;
        results.skippedIds.push(migration.id);
      } else if (!isDryRun) {
        results.ran++;
      }

      if (!dryRun) {
        // Write state THEN update in-memory — if write fails, migration
        // is not marked as applied and will retry on next install
        const now = new Date().toISOString();
        const updatedState = {
          ...state,
          appliedMigrations: [...state.appliedMigrations, migration.id],
          lastRunVersion: pkg.version,
          lastRunAt: now,
          migrationDetails: {
            ...state.migrationDetails,
            [migration.id]: {
              appliedAt: now,
              duration,
              packageVersion: pkg.version,
              result: resultStr,
              status: isSkip ? "skipped" : "applied",
            },
          },
        };
        const writeOk = writeMigrationState(updatedState, stateFile);
        if (writeOk) {
          // Only update in-memory state after successful write
          state.appliedMigrations.push(migration.id);
          state.lastRunVersion = pkg.version;
          state.lastRunAt = now;
          state.migrationDetails = updatedState.migrationDetails;
        }
      }
    } catch (err) {
      logError(`Migration ${migration.id} failed: ${err.message}`);
      results.failed++;
      results.failedIds.push(migration.id);
      // Record failure details in state (best-effort)
      if (!dryRun) {
        state.migrationDetails = state.migrationDetails || {};
        const existing = state.migrationDetails[migration.id] || {};
        state.migrationDetails[migration.id] = {
          ...existing,
          status: "failed",
          lastAttemptAt: new Date().toISOString(),
          lastError: err.message,
          attemptCount: (existing.attemptCount || 0) + 1,
        };
        writeMigrationState(state, stateFile);
      }
    }
  }

  return results;
}

function migration001({ projectDir, dryRun = false }) {
  const claudeDir = path.join(projectDir, ".claude");
  const constantsPath = path.join(claudeDir, "project-constants.md");
  const migratedPath = constantsPath + ".migrated";
  const localPathsPath = path.join(claudeDir, "project-paths.local.md");
  const gitignorePath = path.join(projectDir, ".gitignore");

  if (fs.existsSync(migratedPath)) {
    return { status: "skipped", message: "Already migrated" };
  }

  if (!fs.existsSync(constantsPath)) {
    return { status: "noop", message: "No project-constants.md found" };
  }

  const content = fs.readFileSync(constantsPath, "utf-8");
  const lines = content.split("\n");

  let sectionStart = -1;
  let sectionEnd = lines.length;

  for (let i = 0; i < lines.length; i++) {
    if (/^## Repository Paths\s*$/.test(lines[i])) {
      sectionStart = i;
    } else if (
      sectionStart !== -1 &&
      i > sectionStart &&
      /^## /.test(lines[i])
    ) {
      sectionEnd = i;
      break;
    }
  }

  if (sectionStart === -1) {
    return { status: "noop", message: "No Repository Paths section found" };
  }

  const sectionLines = lines.slice(sectionStart, sectionEnd);

  const tableRows = sectionLines.filter((line) => {
    const trimmed = line.trim();
    return trimmed.startsWith("|") && trimmed.endsWith("|");
  });

  const dataRows = tableRows.filter((line) => {
    const cells = line
      .split("|")
      .slice(1, -1)
      .map((c) => c.trim());
    if (cells.length < 3) return false;
    if (cells.every((c) => /^[-:\s]+$/.test(c))) return false;
    if (cells[0].toLowerCase().includes("repository")) return false;
    return true;
  });

  if (dataRows.length === 0) {
    return { status: "noop", message: "Malformed table" };
  }

  const parsedRows = dataRows.map((line) => {
    const cells = line
      .split("|")
      .slice(1, -1)
      .map((c) => c.trim());
    if (cells.length < 3) return null;
    return cells;
  });

  if (parsedRows.some((r) => r === null)) {
    return { status: "noop", message: "Malformed table" };
  }

  function unescapeMarkdown(str) {
    return str.replace(/\\(.)/g, "$1");
  }

  const maxRepo = Math.max(
    ...parsedRows.map((r) => unescapeMarkdown(r[0]).length),
    "Constant".length,
  );
  const maxPath = Math.max(
    ...parsedRows.map((r) => unescapeMarkdown(r[2]).length),
    "Value".length,
  );
  const maxDesc = Math.max(
    ...parsedRows.map((r) => unescapeMarkdown(r[1]).length),
    "Description".length,
  );

  const pad = (str, len) => str + " ".repeat(Math.max(0, len - str.length));

  const header = `| ${pad("Constant", maxRepo)} | ${pad("Value", maxPath)} | ${pad("Description", maxDesc)} |`;
  const sep = `| ${"-".repeat(maxRepo)} | ${"-".repeat(maxPath)} | ${"-".repeat(maxDesc)} |`;
  const rowLines = parsedRows.map(
    (r) =>
      `| ${pad(unescapeMarkdown(r[0]), maxRepo)} | ${pad(unescapeMarkdown(r[2]), maxPath)} | ${pad(unescapeMarkdown(r[1]), maxDesc)} |`,
  );

  const localPathsContent = [
    "# Project Paths (machine-local)",
    "",
    "Machine-specific repository paths. Gitignored — never commit this file.",
    "",
    header,
    sep,
    ...rowLines,
    "",
  ].join("\n");

  if (dryRun) {
    return {
      status: "applied",
      message: `Migration 001 would apply (dry-run): ${parsedRows.length} row(s) → ${localPathsPath}`,
    };
  }

  fs.writeFileSync(localPathsPath, localPathsContent, "utf-8");

  fs.copyFileSync(constantsPath, migratedPath);

  const beforeSection = lines.slice(0, sectionStart);
  let afterSection = lines.slice(sectionEnd);

  while (
    beforeSection.length > 0 &&
    beforeSection[beforeSection.length - 1].trim() === ""
  ) {
    beforeSection.pop();
  }
  while (afterSection.length > 0 && afterSection[0].trim() === "") {
    afterSection.shift();
  }

  let newLines;
  if (afterSection.length > 0) {
    newLines = [...beforeSection, "", ...afterSection];
  } else {
    newLines = beforeSection;
  }

  fs.writeFileSync(constantsPath, newLines.join("\n"), "utf-8");

  let gitignoreContent = "";
  if (fs.existsSync(gitignorePath)) {
    gitignoreContent = fs.readFileSync(gitignorePath, "utf-8");
  }

  if (!gitignoreContent.includes(".claude/project-paths.local.md")) {
    const separator =
      gitignoreContent.length > 0 && !gitignoreContent.endsWith("\n")
        ? "\n"
        : "";
    fs.writeFileSync(
      gitignorePath,
      gitignoreContent + separator + ".claude/project-paths.local.md\n",
      "utf-8",
    );
  }

  return { status: "applied", message: "Migration 001 applied" };
}

// ─── Migration 020 ──────────────────────────────────────────────────────────
//
// Older installs registered hook commands as `node ~/.claude/hooks/<file>.js`.
// Claude Code expands the tilde, but other agentic tools (e.g. Cortex Code on
// Windows) treat it as a literal path component and resolve relative to CWD,
// producing errors like `Cannot find module '<cwd>/~/.claude/hooks/...`.
// This migration rewrites any `~/.claude/hooks/<file>` references in the
// global settings.json to an absolute, forward-slashed, quoted path.

function rewriteTildeHookPath(cmd, globalDir) {
  if (typeof cmd !== "string") return cmd;
  const absHooks = path.join(globalDir, "hooks").replace(/\\/g, "/");
  assertSafeForShellQuoting(absHooks);
  // Replace `"~/.claude/hooks/<file>"` (already quoted) with the absolute path.
  let result = cmd.replace(
    /"~\/\.claude\/hooks\/([^"\s]+)"/g,
    (_, f) => `"${absHooks}/${f}"`,
  );
  // Replace unquoted `~/.claude/hooks/<file>` with a quoted absolute path.
  result = result.replace(
    /(^|\s)~\/\.claude\/hooks\/([^"\s]+)/g,
    (_, lead, f) => `${lead}"${absHooks}/${f}"`,
  );
  return result;
}

function migration020({ dryRun = false, globalDir = GLOBAL_CLAUDE_DIR } = {}) {
  const settingsPath = path.join(globalDir, "settings.json");
  if (!fs.existsSync(settingsPath)) {
    return "skip: no global settings.json";
  }

  let settings;
  try {
    settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
  } catch {
    return "skip: settings.json is not valid JSON";
  }

  let changed = 0;

  if (settings.statusLine && typeof settings.statusLine.command === "string") {
    const after = rewriteTildeHookPath(settings.statusLine.command, globalDir);
    if (after !== settings.statusLine.command) {
      settings.statusLine.command = after;
      changed++;
    }
  }

  if (settings.hooks && typeof settings.hooks === "object") {
    for (const eventName of Object.keys(settings.hooks)) {
      const entries = settings.hooks[eventName];
      if (!Array.isArray(entries)) continue;
      for (const entry of entries) {
        if (Array.isArray(entry.hooks)) {
          for (const h of entry.hooks) {
            if (typeof h.command === "string") {
              const after = rewriteTildeHookPath(h.command, globalDir);
              if (after !== h.command) {
                h.command = after;
                changed++;
              }
            }
          }
        } else if (typeof entry.command === "string") {
          const after = rewriteTildeHookPath(entry.command, globalDir);
          if (after !== entry.command) {
            entry.command = after;
            changed++;
          }
        }
      }
    }
  }

  if (changed === 0) {
    return "skip: no tilde-based hook paths found";
  }

  if (dryRun) {
    return `dry-run: would rewrite ${changed} hook command(s)`;
  }

  writeSettingsAtomic(settingsPath, JSON.stringify(settings, null, 2) + "\n");
  return `applied: rewrote ${changed} hook command(s)`;
}

module.exports = {
  MIGRATIONS,
  readMigrationState,
  writeMigrationState,
  runMigrations,
  migration001,
  migration020,
  rewriteTildeHookPath,
};
