// src/install/blocks/ConfigFileWriter.ts
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import type { ConfigShape, ConfigMutation, StepReceipt } from "../Installer";
import { parsers } from "./parsers";
import { atomicWrite } from "./atomicWrite";
import { backupFile } from "./backup";

export interface ConfigFilePlan {
  path: string;
  shape: ConfigShape;
  mutations: ConfigMutation[];
  priorContent: string | null;
  priorChecksum: string | null;
}

/**
 * Get (or create) a nested path in an object, returning the leaf container and key.
 */
function dig(
  root: Record<string, unknown>,
  path: string[],
): { container: Record<string, unknown>; key: string } {
  let cur: Record<string, unknown> = root;
  for (let i = 0; i < path.length - 1; i++) {
    const seg = path[i]!;
    if (cur[seg] == null || typeof cur[seg] !== "object") {
      cur[seg] = {};
    }
    cur = cur[seg] as Record<string, unknown>;
  }
  return { container: cur, key: path[path.length - 1]! };
}

function deepEqual(a: unknown, b: unknown): boolean {
  return JSON.stringify(a) === JSON.stringify(b);
}

type HookEntry = { matcher?: string; hooks: Array<{ type: string; command: string; timeout?: number }> };

function ensureHookArray(root: Record<string, unknown>, path: string[]): HookEntry[] {
  let cur: Record<string, unknown> = root;
  for (let i = 0; i < path.length - 1; i++) {
    const seg = path[i]!;
    if (cur[seg] == null || typeof cur[seg] !== "object") cur[seg] = {};
    cur = cur[seg] as Record<string, unknown>;
  }
  const leaf = path[path.length - 1]!;
  if (!Array.isArray(cur[leaf])) cur[leaf] = [];
  return cur[leaf] as HookEntry[];
}

function applyHookArrayMerge(
  root: Record<string, unknown>,
  mut: Extract<ConfigMutation, { op: "hook-array-merge" }>,
): boolean {
  const arr = ensureHookArray(root, mut.path);
  if (mut.matcher !== undefined) {
    let block = arr.find((e) => e.matcher === mut.matcher);
    if (!block) {
      block = { matcher: mut.matcher, hooks: [] };
      arr.push(block);
    }
    const exists = block.hooks.some((h) => h.command === mut.dedupeBy.command);
    if (exists) return false;
    if (mut.position === "first") {
      block.hooks.unshift(mut.entry);
    } else {
      block.hooks.push(mut.entry);
    }
    return true;
  }
  // No matcher (e.g. SessionStart).
  const exists = arr.some((e) => e.hooks?.some((h) => h.command === mut.dedupeBy.command));
  if (exists) return false;
  arr.push({ hooks: [mut.entry] });
  return true;
}

function applyHookArrayDelete(
  root: Record<string, unknown>,
  mut: Extract<ConfigMutation, { op: "hook-array-delete" }>,
): boolean {
  const arr = ensureHookArray(root, mut.path);
  if (mut.predicate.kind === "matcher-equals") {
    const target = mut.predicate.matcher;
    const before = arr.length;
    const filtered = arr.filter((e) => e.matcher !== target);
    if (filtered.length === before) return false;
    arr.length = 0;
    arr.push(...filtered);
    return true;
  }
  let changed = false;
  const matchEntry = (h: { command: string }) =>
    mut.predicate.kind === "command-equals"
      ? h.command === (mut.predicate as { command: string }).command
      : h.command?.endsWith((mut.predicate as { suffix: string }).suffix);
  for (const block of arr) {
    if (mut.matcher !== undefined && block.matcher !== mut.matcher) continue;
    const before = block.hooks.length;
    block.hooks = block.hooks.filter((h) => !matchEntry(h));
    if (block.hooks.length !== before) changed = true;
  }
  // Drop empty blocks
  const surviving = arr.filter((e) => e.hooks && e.hooks.length > 0);
  if (surviving.length !== arr.length) {
    arr.length = 0;
    arr.push(...surviving);
    changed = true;
  }
  return changed;
}

function applyMutation(root: Record<string, unknown>, mut: ConfigMutation): boolean {
  if (mut.op === "hook-array-merge") return applyHookArrayMerge(root, mut);
  if (mut.op === "hook-array-delete") return applyHookArrayDelete(root, mut);
  if (mut.path.length === 0) return false;
  const { container, key } = dig(root, mut.path);
  if (mut.op === "set") {
    if (deepEqual(container[key], mut.value)) return false;
    container[key] = mut.value;
    return true;
  } else if (mut.op === "delete") {
    if (!(key in container)) return false;
    delete container[key];
    return true;
  } else if (mut.op === "merge") {
    if (container[key] == null || typeof container[key] !== "object") {
      container[key] = { ...mut.value };
      return true;
    }
    const existing = container[key] as Record<string, unknown>;
    let changed = false;
    for (const [k, v] of Object.entries(mut.value)) {
      if (!deepEqual(existing[k], v)) {
        existing[k] = v;
        changed = true;
      }
    }
    return changed;
  }
  return false;
}

export class ConfigFileWriter {
  constructor(
    private readonly path: string,
    private readonly shape: ConfigShape,
  ) {}

  async plan(mutations: ConfigMutation[]): Promise<ConfigFilePlan> {
    const prior = existsSync(this.path)
      ? await readFile(this.path, "utf8")
      : null;
    const priorChecksum = prior
      ? createHash("sha256").update(prior).digest("hex")
      : null;

    // Apply mutations to a copy to see which ones change state
    const parsed = prior
      ? (parsers[this.shape].parse(prior) as Record<string, unknown>)
      : {};

    const effectiveMutations = mutations.filter((m) => {
      // Clone so we can test idempotency
      const clone = JSON.parse(JSON.stringify(parsed)) as Record<string, unknown>;
      return applyMutation(clone, m);
    });

    return { path: this.path, shape: this.shape, mutations: effectiveMutations, priorContent: prior, priorChecksum };
  }

  async apply(
    plan: ConfigFilePlan,
    backupDir: string,
  ): Promise<Extract<StepReceipt, { kind: "config-file" }>> {
    const backupPath = plan.priorContent !== null ? (backupFile(this.path, backupDir) ?? "") : "";

    const parsed = plan.priorContent
      ? (parsers[this.shape].parse(plan.priorContent) as Record<string, unknown>)
      : ({} as Record<string, unknown>);

    for (const m of plan.mutations) {
      applyMutation(parsed, m);
    }

    const newContent = parsers[this.shape].serialize(parsed);
    await atomicWrite(this.path, newContent);
    const postChecksum = createHash("sha256").update(newContent).digest("hex");

    return {
      kind: "config-file",
      path: this.path,
      backupPath,
      priorContent: plan.priorContent,
      postChecksum,
    };
  }

  async revertForce(receipt: Extract<StepReceipt, { kind: "config-file" }>): Promise<void> {
    if (receipt.priorContent === null) {
      // File didn't exist before; remove it
      const { unlinkSync, existsSync: es } = await import("node:fs");
      if (es(this.path)) unlinkSync(this.path);
      return;
    }
    await atomicWrite(this.path, receipt.priorContent);
  }
}
