import { spawnSync } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import {
  chmodSync,
  closeSync,
  existsSync,
  fsyncSync,
  lstatSync,
  mkdirSync,
  openSync,
  readFileSync,
  realpathSync,
  renameSync,
  rmSync,
  writeFileSync,
} from "node:fs";
import { homedir } from "node:os";
import { DatabaseSync } from "./sqlite.ts";
import { dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
import {
  KxmConfigError,
  KxmSchemaRegistry,
  parseRestrictedYaml,
  type JsonObject,
  type KxmConfigIssue,
} from "./project-config.ts";

const MAX_BINDING_RECORD_BYTES = 256 * 1024;
const BINDING_LABEL = "Runtime-local repository bindings";

export interface KxmLocalBindingRecord {
  schema: "kxm.local-repository-bindings.v1";
  projectId: string;
  projectRoot: string;
  repositories: Readonly<Record<string, string>>;
}

export interface LoadedKxmLocalBindings extends KxmLocalBindingRecord {
  file: string;
}

export interface KxmLocalBindingPathOptions {
  stateRoot?: string;
  env?: NodeJS.ProcessEnv;
  homeDir?: string;
  platform?: NodeJS.Platform;
}

export interface KxmLocalBindingStoreOptions extends KxmLocalBindingPathOptions {
  schemasDir?: string;
}

export interface KxmLocalBindingWriteResult {
  file: string;
  written: boolean;
  record: KxmLocalBindingRecord;
}

export interface KxmLocalBindingLock {
  readonly projectRoot: string;
  readonly file: string;
}

const activeLocks = new WeakSet<KxmLocalBindingLock>();

function bindingIssue(phase: KxmConfigIssue["phase"], code: string, message: string): KxmConfigIssue {
  return { phase, code, file: BINDING_LABEL, message };
}

function bindingError(phase: KxmConfigIssue["phase"], code: string, message: string): never {
  throw new KxmConfigError([bindingIssue(phase, code, message)]);
}

export function canonicalHostPath(path: string): string {
  const absolute = resolve(path);
  try {
    return realpathSync.native(absolute);
  } catch {
    return absolute;
  }
}

export function sameHostPath(left: string, right: string, platform = process.platform): boolean {
  const first = canonicalHostPath(left);
  const second = canonicalHostPath(right);
  return platform === "win32"
    ? first.toLocaleLowerCase("en-US") === second.toLocaleLowerCase("en-US")
    : first === second;
}

/** Resolve the per-user state root without consulting any legacy KXM_* workspace override. */
export function kxmUserStateRoot(options: KxmLocalBindingPathOptions = {}): string {
  const env = options.env ?? process.env;
  const platform = options.platform ?? process.platform;
  const home = options.homeDir ?? homedir();
  const explicit = options.stateRoot ?? env.KXM_STATE_HOME?.trim();
  if (explicit) {
    if (!isAbsolute(explicit)) bindingError("path", "local_state_root_not_absolute", "KXM_STATE_HOME must be an absolute host-local path");
    return resolve(explicit);
  }
  if (platform === "win32") {
    const localAppData = env.LOCALAPPDATA?.trim();
    const base = localAppData && isAbsolute(localAppData) ? localAppData : join(home, "AppData", "Local");
    return resolve(base, "KXM");
  }
  if (platform === "darwin") return resolve(home, "Library", "Application Support", "KXM");
  const xdgState = env.XDG_STATE_HOME?.trim();
  const base = xdgState && isAbsolute(xdgState) ? xdgState : join(home, ".local", "state");
  return resolve(base, "kxm");
}

function projectBindingKey(projectRoot: string, platform = process.platform): string {
  const canonical = canonicalHostPath(projectRoot);
  const keyInput = platform === "win32" ? canonical.toLocaleLowerCase("en-US") : canonical;
  return createHash("sha256").update(keyInput, "utf8").digest("hex");
}

export function kxmLocalBindingFile(projectRoot: string, options: KxmLocalBindingPathOptions = {}): string {
  const stateRoot = kxmUserStateRoot(options);
  return join(stateRoot, "projects", projectBindingKey(projectRoot, options.platform), "repository-bindings.json");
}

function checkedDirectory(path: string, description: string): boolean {
  if (!existsSync(path)) return false;
  const stat = lstatSync(path);
  if (stat.isSymbolicLink() || !stat.isDirectory()) {
    bindingError("path", "local_binding_directory_invalid", `${description} must be a regular directory, not a link or file`);
  }
  return true;
}

function existingBindingDirectory(file: string, stateRoot: string): boolean {
  const projects = join(stateRoot, "projects");
  const project = dirname(file);
  if (!checkedDirectory(stateRoot, "local state root")) return false;
  if (!checkedDirectory(projects, "local projects directory")) return false;
  return checkedDirectory(project, "local project binding directory");
}

function asRecord(value: JsonObject, file: string, projectRoot: string, schemasDir?: string): LoadedKxmLocalBindings {
  const registry = new KxmSchemaRegistry(schemasDir);
  const issues = registry.validateLocalBindings(value, BINDING_LABEL);
  if (issues.length > 0) throw new KxmConfigError(issues);
  const recordRoot = String(value.projectRoot);
  if (!sameHostPath(recordRoot, projectRoot)) {
    bindingError("semantic", "local_binding_project_root_mismatch", "binding record belongs to a different control worktree");
  }
  const repositories = Object.fromEntries(Object.entries(value.repositories as Record<string, string>)
    .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0));
  return {
    schema: "kxm.local-repository-bindings.v1",
    projectId: String(value.projectId),
    projectRoot: recordRoot,
    repositories,
    file,
  };
}

/** Read one exact, bounded Runtime-local binding record. Missing state is not an error. */
export function readKxmLocalBindings(
  projectRoot: string,
  options: KxmLocalBindingStoreOptions = {},
): LoadedKxmLocalBindings | undefined {
  const root = resolve(projectRoot);
  const stateRoot = kxmUserStateRoot(options);
  const file = kxmLocalBindingFile(root, options);
  if (!existingBindingDirectory(file, stateRoot) || !existsSync(file)) return undefined;
  const stat = lstatSync(file);
  if (stat.isSymbolicLink() || !stat.isFile()) {
    bindingError("path", "local_binding_file_invalid", "binding record must be a regular file, not a link or directory");
  }
  if (stat.size > MAX_BINDING_RECORD_BYTES) {
    bindingError("parse", "local_binding_file_too_large", `binding record exceeds ${MAX_BINDING_RECORD_BYTES} bytes`);
  }
  const parsed = parseRestrictedYaml(readFileSync(file), BINDING_LABEL);
  return asRecord(parsed, file, root, options.schemasDir);
}

function normalizedRecord(
  projectRoot: string,
  projectId: string,
  repositories: Readonly<Record<string, string>>,
  schemasDir?: string,
): KxmLocalBindingRecord {
  const normalizedRepositories = Object.fromEntries(Object.entries(repositories)
    .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
    .map(([repositoryId, path]) => {
      if (!isAbsolute(path)) bindingError("path", "repository_binding_not_absolute", `host-local binding for ${repositoryId} must be absolute`);
      return [repositoryId, canonicalHostPath(path)];
    }));
  const value: JsonObject = {
    schema: "kxm.local-repository-bindings.v1",
    projectId,
    projectRoot: canonicalHostPath(projectRoot),
    repositories: normalizedRepositories,
  };
  const issues = new KxmSchemaRegistry(schemasDir).validateLocalBindings(value, BINDING_LABEL);
  if (issues.length > 0) throw new KxmConfigError(issues);
  return value as unknown as KxmLocalBindingRecord;
}

function recordsEqual(left: KxmLocalBindingRecord, right: KxmLocalBindingRecord): boolean {
  if (left.projectId !== right.projectId || !sameHostPath(left.projectRoot, right.projectRoot)) return false;
  const leftEntries = Object.entries(left.repositories);
  const rightEntries = Object.entries(right.repositories);
  return leftEntries.length === rightEntries.length && leftEntries.every(([id, path], index) => {
    const other = rightEntries[index];
    return other?.[0] === id && sameHostPath(path, other[1]);
  });
}

function syncDirectory(path: string): void {
  let descriptor: number | undefined;
  try {
    descriptor = openSync(path, "r");
    fsyncSync(descriptor);
  } catch (error) {
    const code = (error as NodeJS.ErrnoException).code;
    if (process.platform !== "win32" && code !== "EINVAL" && code !== "ENOTSUP" && code !== "EISDIR") throw error;
  } finally {
    if (descriptor !== undefined) closeSync(descriptor);
  }
}

function createDirectoryIfMissing(path: string): void {
  if (existsSync(path)) return;
  try {
    mkdirSync(path, { mode: 0o700 });
    syncDirectory(dirname(path));
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
  }
}

function ensureDurableDirectory(path: string, description: string): void {
  const missing: string[] = [];
  let current = resolve(path);
  while (!existsSync(current)) {
    missing.push(current);
    const parent = dirname(current);
    if (parent === current) bindingError("path", "local_binding_directory_invalid", `${description} has no existing filesystem ancestor`);
    current = parent;
  }
  checkedDirectory(current, `${description} ancestor`);
  for (const directory of missing.reverse()) {
    mkdirSync(directory, { mode: 0o700 });
    checkedDirectory(directory, description);
    syncDirectory(dirname(directory));
  }
}

function ensureBindingDirectory(file: string, stateRoot: string): void {
  ensureDurableDirectory(stateRoot, "local state root");
  checkedDirectory(stateRoot, "local state root");
  syncDirectory(stateRoot);
  const projects = join(stateRoot, "projects");
  createDirectoryIfMissing(projects);
  checkedDirectory(projects, "local projects directory");
  const project = dirname(file);
  createDirectoryIfMissing(project);
  checkedDirectory(project, "local project binding directory");
}

function assertNoLinkedDirectoryComponents(path: string, description: string): void {
  const absolute = resolve(path);
  const filesystemRoot = parse(absolute).root;
  let current = filesystemRoot;
  const remainder = relative(filesystemRoot, absolute);
  for (const segment of remainder.split(sep).filter(Boolean)) {
    current = join(current, segment);
    const stat = lstatSync(current);
    if (stat.isSymbolicLink() || !stat.isDirectory()) {
      bindingError("path", "project_operation_lock_parent_invalid", `${description} contains a linked or non-directory component`);
    }
  }
}

function projectOperationLockFile(projectRoot: string): string {
  const env = { ...process.env };
  for (const key of Object.keys(env)) {
    if (key.toLocaleUpperCase("en-US").startsWith("GIT_")) delete env[key];
  }
  const result = spawnSync("git", ["-C", projectRoot, "rev-parse", "--absolute-git-dir"], {
    encoding: "utf8",
    env,
    timeout: 5000,
    windowsHide: true,
  });
  if (result.status !== 0 || result.error || !result.stdout.trim()) {
    bindingError("discovery", "project_operation_lock_unavailable", "cannot resolve the authoritative Git metadata path for project locking");
  }
  const gitDirectory = resolve(result.stdout.trim());
  assertNoLinkedDirectoryComponents(gitDirectory, "authoritative Git metadata path");
  const gitStat = lstatSync(gitDirectory);
  if (gitStat.isSymbolicLink() || !gitStat.isDirectory()) {
    bindingError("path", "project_operation_lock_parent_invalid", "authoritative Git metadata must resolve to a regular directory");
  }
  const lockDirectory = join(gitDirectory, "kxm");
  if (existsSync(lockDirectory)) {
    const stat = lstatSync(lockDirectory);
    if (stat.isSymbolicLink() || !stat.isDirectory()) {
      bindingError("path", "project_operation_lock_parent_invalid", "Git-local KXM lock directory must be regular, not a link or file");
    }
  } else {
    mkdirSync(lockDirectory, { mode: 0o700 });
    syncDirectory(gitDirectory);
  }
  return join(lockDirectory, "project-operation-lock.sqlite");
}

/** Serialize one project mutation with a process-death-released SQLite write lock. */
export function withKxmLocalBindingLock<T>(
  projectRoot: string,
  options: KxmLocalBindingStoreOptions,
  callback: (lock: KxmLocalBindingLock) => T,
): T {
  const root = resolve(projectRoot);
  const file = projectOperationLockFile(root);
  if (existsSync(file)) {
    const stat = lstatSync(file);
    if (stat.isSymbolicLink() || !stat.isFile()) {
      bindingError("path", "local_binding_lock_invalid", "project-operation lock must be a regular file, not a link or directory");
    }
  }
  const database = new DatabaseSync(file);
  try { chmodSync(file, 0o600); } catch { /* Windows and restrictive filesystems may ignore POSIX modes. */ }
  try {
    try {
      database.exec("PRAGMA busy_timeout = 0; BEGIN IMMEDIATE");
    } catch (error) {
      if (/busy|locked/i.test(error instanceof Error ? error.message : String(error))) {
        bindingError("semantic", "local_binding_lock_busy", "another project initialization or binding update is active");
      }
      throw error;
    }
    const lock: KxmLocalBindingLock = Object.freeze({ projectRoot: canonicalHostPath(root), file });
    activeLocks.add(lock);
    try {
      return callback(lock);
    } finally {
      activeLocks.delete(lock);
      database.exec("ROLLBACK");
    }
  } finally {
    database.close();
  }
}

export function planKxmLocalBindings(
  projectRoot: string,
  projectId: string,
  repositories: Readonly<Record<string, string>>,
  options: KxmLocalBindingStoreOptions = {},
): KxmLocalBindingWriteResult {
  const root = resolve(projectRoot);
  const file = kxmLocalBindingFile(root, options);
  const record = normalizedRecord(root, projectId, repositories, options.schemasDir);
  const existing = readKxmLocalBindings(root, options);
  if (existing?.projectId !== undefined && existing.projectId !== projectId) {
    bindingError("semantic", "local_binding_project_id_mismatch", "binding record belongs to a different project identity");
  }
  return { file, written: !existing || !recordsEqual(existing, record), record };
}

/** Atomically persist validated host paths outside the Git-tracked project. */
export function writeKxmLocalBindings(
  projectRoot: string,
  projectId: string,
  repositories: Readonly<Record<string, string>>,
  options: KxmLocalBindingStoreOptions = {},
  lock?: KxmLocalBindingLock,
): KxmLocalBindingWriteResult {
  const root = resolve(projectRoot);
  if (!lock) {
    return withKxmLocalBindingLock(root, options, (acquired) => writeKxmLocalBindings(root, projectId, repositories, options, acquired));
  }
  if (!activeLocks.has(lock) || !sameHostPath(lock.projectRoot, root)) {
    bindingError("semantic", "local_binding_lock_invalid", "binding update does not hold the active project lock");
  }
  const planned = planKxmLocalBindings(root, projectId, repositories, options);
  const { file, record } = planned;
  if (!planned.written) return planned;
  ensureBindingDirectory(file, kxmUserStateRoot(options));
  const temporary = join(dirname(file), `.repository-bindings-${process.pid}-${randomUUID()}.tmp`);
  let descriptor: number | undefined;
  try {
    descriptor = openSync(temporary, "wx", 0o600);
    writeFileSync(descriptor, `${JSON.stringify(record, null, 2)}\n`, "utf8");
    fsyncSync(descriptor);
    closeSync(descriptor);
    descriptor = undefined;
    renameSync(temporary, file);
    syncDirectory(dirname(file));
  } finally {
    if (descriptor !== undefined) closeSync(descriptor);
    rmSync(temporary, { force: true });
  }
  const verified = readKxmLocalBindings(root, options);
  if (!verified || !recordsEqual(verified, record)) {
    bindingError("semantic", "local_binding_install_verification_failed", "installed binding record does not match the validated input");
  }
  return { file, written: true, record };
}
