import { spawn } from "node:child_process";
import { createHash } from "node:crypto";
import { chmod, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
import { isAbsolute, join, relative, resolve, sep } from "node:path";

import { Client } from "pg";

import type { BackupJobRecord } from "@/application/phase9/backup-jobs";
import {
  BACKUP_FORMAT_VERSION,
  backupsToDelete,
  safeBackupSegment,
} from "@/application/phase9/backup-policy";
import {
  findPostgresTool,
  postgresProcessEnvironment,
} from "@/infrastructure/backup/postgres-tools";

const PORTABLE_TABLES = [
  "accounts",
  "service_credentials",
  "rule_sets",
  "ranking_identities",
  "migration_batches",
  "migration_source_rows",
  "activities",
  "activity_participants",
  "identity_resolution_records",
  "screenshot_name_hints",
  "rotation_groups",
  "rotation_group_results",
  "partial_rotation_group_results",
  "rotation_appearances",
  "activity_alias_mappings",
  "activity_events",
  "event_results",
  "activity_revisions",
  "point_entries",
  "point_entry_reversals",
  "settlement_previews",
  "activity_fingerprints",
  "activity_drafts",
  "audit_logs",
  "system_settings",
  "backup_jobs",
] as const;

const SECRET_KEY = /(password|token|secret|credential|csrf|session).*?(hash|value)?$/i;

export interface BackupArtifactResult {
  readonly artifactName: string;
  readonly artifactSha256: string;
  readonly portableExportSha256: string;
  readonly directory: string;
  readonly deletedArtifacts: readonly string[];
}

interface PortableExport {
  readonly format: typeof BACKUP_FORMAT_VERSION;
  readonly exportedAt: string;
  readonly sourceDatabase: string;
  readonly tables: Readonly<Record<string, readonly unknown[]>>;
  readonly rowCounts: Readonly<Record<string, number>>;
}

function backupRoot(): string {
  const configured = process.env.BACKUP_DIRECTORY?.trim();
  if (configured) {
    if (!isAbsolute(configured)) throw new Error("BACKUP_DIRECTORY_MUST_BE_ABSOLUTE");
    return resolve(configured);
  }
  const localAppData = process.env.LOCALAPPDATA?.trim();
  return localAppData
    ? resolve(localAppData, "Chengyuge", "backups")
    : resolve(process.cwd(), ".phase9-private", "backups");
}

function withinRoot(root: string, candidate: string): boolean {
  const difference = relative(root, candidate);
  return (
    difference !== "" &&
    difference !== ".." &&
    !difference.startsWith(`..${sep}`) &&
    !isAbsolute(difference)
  );
}

async function sha256(path: string): Promise<string> {
  return createHash("sha256")
    .update(await readFile(path))
    .digest("hex");
}

function sanitize(value: unknown): unknown {
  if (Array.isArray(value)) return value.map(sanitize);
  if (value && typeof value === "object") {
    const output: Record<string, unknown> = {};
    for (const [key, child] of Object.entries(value)) {
      if (!SECRET_KEY.test(key)) output[key] = sanitize(child);
    }
    return output;
  }
  return value;
}

async function writePortableExport(
  client: Client,
  path: string,
  now: Date,
): Promise<PortableExport> {
  const databaseResult = await client.query<{ name: string }>("select current_database() as name");
  const tables: Record<string, readonly unknown[]> = {};
  const rowCounts: Record<string, number> = {};
  for (const table of PORTABLE_TABLES) {
    const result = await client.query<{ record: unknown }>(
      `select to_jsonb(source) as record from \"${table}\" as source order by to_jsonb(source)::text`,
    );
    tables[table] = result.rows.map((row) => sanitize(row.record));
    rowCounts[table] = result.rowCount ?? result.rows.length;
  }
  const exported: PortableExport = {
    format: BACKUP_FORMAT_VERSION,
    exportedAt: now.toISOString(),
    sourceDatabase: databaseResult.rows[0]?.name ?? "unknown",
    tables,
    rowCounts,
  };
  await writeFile(path, `${JSON.stringify(exported, null, 2)}\n`, {
    encoding: "utf8",
    mode: 0o600,
  });
  return exported;
}

async function runPgDump(
  connectionString: string,
  output: string,
  snapshotId: string,
): Promise<void> {
  const tool = await findPostgresTool("pg_dump");
  const { database, environment } = postgresProcessEnvironment(connectionString);
  await new Promise<void>((resolvePromise, reject) => {
    const child = spawn(
      tool,
      [
        "--format=custom",
        "--no-owner",
        "--no-acl",
        `--snapshot=${snapshotId}`,
        "--file",
        output,
        database,
      ],
      { env: environment, windowsHide: true, stdio: ["ignore", "ignore", "pipe"] },
    );
    let stderr = "";
    child.stderr.setEncoding("utf8");
    child.stderr.on("data", (chunk: string) => {
      if (stderr.length < 8_192) stderr += chunk;
    });
    child.once("error", () => reject(new Error("PG_DUMP_START_FAILED")));
    child.once("close", (code) => {
      if (code === 0) resolvePromise();
      else reject(new Error(stderr.trim() ? "PG_DUMP_FAILED" : "PG_DUMP_FAILED"));
    });
  });
}

async function harden(path: string, mode: number): Promise<void> {
  await chmod(path, mode).catch(() => undefined);
}

async function pruneBackups(root: string, now: Date): Promise<readonly string[]> {
  const candidates: { name: string; createdAt: Date }[] = [];
  for (const entry of await readdir(root, { withFileTypes: true })) {
    if (!entry.isDirectory() || !/^chengyuge-[A-Za-z0-9._-]+$/.test(entry.name)) continue;
    const candidate = resolve(root, entry.name);
    const details = await stat(candidate);
    candidates.push({
      name: entry.name,
      createdAt: details.birthtimeMs ? details.birthtime : details.mtime,
    });
  }
  const removed: string[] = [];
  for (const candidate of backupsToDelete(candidates, now)) {
    const target = resolve(root, candidate.name);
    if (!withinRoot(root, target)) throw new Error("BACKUP_RETENTION_PATH_REJECTED");
    await rm(target, { recursive: true, force: false });
    removed.push(candidate.name);
  }
  return removed;
}

export async function createBackupArtifact(
  connectionString: string,
  job: BackupJobRecord,
  now = new Date(),
): Promise<BackupArtifactResult> {
  const root = backupRoot();
  await mkdir(root, { recursive: true, mode: 0o700 });
  await harden(root, 0o700);
  const stamp = now
    .toISOString()
    .replace(/[-:]/g, "")
    .replace(/\.\d{3}Z$/, "Z");
  const artifactName = `chengyuge-${stamp}-${safeBackupSegment(job.triggerType.toLowerCase())}-${safeBackupSegment(job.id)}`;
  const finalDirectory = resolve(root, artifactName);
  const partialDirectory = resolve(root, `.partial-${safeBackupSegment(job.id)}`);
  if (!withinRoot(root, partialDirectory) || !withinRoot(root, finalDirectory)) {
    throw new Error("BACKUP_PATH_REJECTED");
  }
  await rm(partialDirectory, { recursive: true, force: true });
  await mkdir(partialDirectory, { mode: 0o700 });
  try {
    const dumpPath = join(partialDirectory, "database.dump");
    const portablePath = join(partialDirectory, "portable-export.json");
    const snapshotClient = new Client({ connectionString });
    await snapshotClient.connect();
    let portable: PortableExport;
    try {
      await snapshotClient.query("begin transaction isolation level repeatable read read only");
      const snapshot = await snapshotClient.query<{ id: string }>(
        "select pg_export_snapshot() as id",
      );
      const snapshotId = snapshot.rows[0]?.id;
      if (!snapshotId) throw new Error("BACKUP_SNAPSHOT_FAILED");
      await runPgDump(connectionString, dumpPath, snapshotId);
      portable = await writePortableExport(snapshotClient, portablePath, now);
      await snapshotClient.query("commit");
    } catch (error) {
      await snapshotClient.query("rollback").catch(() => undefined);
      throw error;
    } finally {
      await snapshotClient.end();
    }
    const artifactSha256 = await sha256(dumpPath);
    const portableExportSha256 = await sha256(portablePath);
    const manifest = {
      format: BACKUP_FORMAT_VERSION,
      createdAt: now.toISOString(),
      job: {
        id: job.id,
        triggerType: job.triggerType,
        triggerReference: job.triggerReference,
        requestedAt: job.requestedAt.toISOString(),
      },
      files: {
        "database.dump": { sha256: artifactSha256 },
        "portable-export.json": { sha256: portableExportSha256 },
      },
      rowCounts: portable.rowCounts,
    };
    await writeFile(
      join(partialDirectory, "manifest.json"),
      `${JSON.stringify(manifest, null, 2)}\n`,
      {
        encoding: "utf8",
        mode: 0o600,
      },
    );
    for (const name of ["database.dump", "portable-export.json", "manifest.json"]) {
      await harden(join(partialDirectory, name), 0o600);
    }
    await rename(partialDirectory, finalDirectory);
    const deletedArtifacts = await pruneBackups(root, now);
    return {
      artifactName,
      artifactSha256,
      portableExportSha256,
      directory: finalDirectory,
      deletedArtifacts,
    };
  } catch (error) {
    await rm(partialDirectory, { recursive: true, force: true }).catch(() => undefined);
    throw error;
  }
}

export function configuredBackupRoot(): string {
  return backupRoot();
}
