import { createHash, type Hash } from "node:crypto";
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { spawnSync } from "node:child_process";

import { normalizeTodoReminderNotification, normalizeTodoSnapshotEntry, type TodoSnapshot } from "./repository";

interface GitResult {
  ok: boolean;
  stdout: string;
  stderr: string;
  exitCode: number;
}

export interface TodoGitStorageConfig {
  primary: "postgres" | "github-repo";
  repo: string;
  repoSshUrl: string;
  branch: string;
  basePath: string;
  worktreePath: string;
  sshCommand: string;
  authorName: string;
  authorEmail: string;
  commitMessagePrefix: string;
}

export class GitStorageError extends Error {
  readonly detail: Record<string, unknown>;

  constructor(message: string, detail: Record<string, unknown> = {}) {
    super(message);
    this.name = "GitStorageError";
    this.detail = detail;
  }
}

function envString(name: string, fallback: string): string {
  const value = process.env[name];
  return value === undefined || value.length === 0 ? fallback : value;
}

export function gitStorageConfigFromEnv(): TodoGitStorageConfig {
  const primary = envString("TODO_NOTE_STORAGE_PRIMARY", "postgres");
  if (primary !== "postgres" && primary !== "github-repo") throw new Error("TODO_NOTE_STORAGE_PRIMARY must be postgres or github-repo");
  const basePath = envString("TODO_NOTE_GIT_BASE_PATH", "todo-note").replace(/^\/+|\/+$/gu, "");
  if (!basePath || basePath === "." || basePath.split("/").includes("..")) {
    throw new Error("TODO_NOTE_GIT_BASE_PATH must be a non-root repository path");
  }
  return {
    primary,
    repo: envString("TODO_NOTE_GIT_REPO", "pikasTech/decision-center-data"),
    repoSshUrl: envString("TODO_NOTE_GIT_REPO_SSH_URL", ""),
    branch: envString("TODO_NOTE_GIT_BRANCH", "main"),
    basePath,
    worktreePath: envString("TODO_NOTE_GIT_WORKTREE", "/var/lib/unidesk/todo-note/repo"),
    sshCommand: envString("GIT_SSH_COMMAND", ""),
    authorName: envString("TODO_NOTE_GIT_AUTHOR_NAME", "UniDesk Todo Note"),
    authorEmail: envString("TODO_NOTE_GIT_AUTHOR_EMAIL", "todo-note@unidesk.local"),
    commitMessagePrefix: envString("TODO_NOTE_GIT_COMMIT_PREFIX", "todo-note:"),
  };
}

function updateStableJson(hash: Hash, input: unknown, arrayValue = false): void {
  let value = input;
  if (typeof value === "object" && value !== null && typeof (value as { toJSON?: unknown }).toJSON === "function") {
    value = (value as { toJSON: () => unknown }).toJSON();
  }
  if (Array.isArray(value)) {
    hash.update("[");
    value.forEach((item, index) => {
      if (index > 0) hash.update(",");
      updateStableJson(hash, item, true);
    });
    hash.update("]");
    return;
  }
  if (typeof value === "object" && value !== null) {
    hash.update("{");
    const entries = Object.entries(value as Record<string, unknown>)
      .filter(([, item]) => item !== undefined && typeof item !== "function" && typeof item !== "symbol")
      .sort(([left], [right]) => left.localeCompare(right));
    entries.forEach(([key, item], index) => {
      if (index > 0) hash.update(",");
      hash.update(JSON.stringify(key));
      hash.update(":");
      updateStableJson(hash, item);
    });
    hash.update("}");
    return;
  }
  const encoded = JSON.stringify(value);
  if (encoded === undefined) {
    if (arrayValue) hash.update("null");
    return;
  }
  hash.update(encoded);
}

export function snapshotHash(snapshot: TodoSnapshot): string {
  const comparable = { schemaVersion: snapshot.schemaVersion, instances: snapshot.instances, reminderNotifications: snapshot.reminderNotifications };
  const hash = createHash("sha256");
  updateStableJson(hash, comparable);
  return hash.digest("hex");
}

function safeId(value: string): string {
  if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new GitStorageError("Todo instance id is not safe for Git storage", { instanceId: value });
  return value;
}

export class TodoGitStorage {
  private queue: Promise<unknown> = Promise.resolve();

  constructor(readonly config: TodoGitStorageConfig) {
    if (config.primary === "github-repo" && !config.repoSshUrl) throw new Error("TODO_NOTE_GIT_REPO_SSH_URL is required for github-repo storage");
  }

  enabled(): boolean {
    return this.config.primary === "github-repo";
  }

  summary(): Record<string, unknown> {
    return {
      primary: this.config.primary,
      repo: this.config.repo,
      branch: this.config.branch,
      basePath: this.config.basePath,
      worktreePath: this.config.worktreePath,
      sshConfigured: this.config.sshCommand.length > 0,
      head: this.headSummary(),
    };
  }

  async readSnapshot(): Promise<TodoSnapshot | null> {
    if (!this.enabled()) return null;
    return await this.enqueue(async () => {
      this.syncWorktree();
      const indexPath = this.path("indexes", "instances.json");
      if (!existsSync(indexPath)) return null;
      const index = JSON.parse(readFileSync(indexPath, "utf8")) as { schemaVersion?: unknown; instances?: unknown; reminderNotificationsPath?: unknown };
      if (index.schemaVersion !== 1 || !Array.isArray(index.instances)) throw new GitStorageError("Todo Git index has an unsupported schema");
      const seenInstanceIds = new Set<string>();
      const instances = index.instances.map((raw) => {
        if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new GitStorageError("Todo Git index entry is invalid");
        const entry = raw as { id?: unknown; instancePath?: unknown; historyPath?: unknown; source?: unknown };
        if (typeof entry.id !== "string" || typeof entry.instancePath !== "string" || typeof entry.historyPath !== "string") {
          throw new GitStorageError("Todo Git index entry is missing paths");
        }
        const id = safeId(entry.id);
        const instancePath = join(this.config.basePath, "instances", `${id}.json`);
        const historyPath = join(this.config.basePath, "history", `${id}.json`);
        if (entry.instancePath !== instancePath || entry.historyPath !== historyPath) {
          throw new GitStorageError("Todo Git index entry points outside its managed instance paths", {
            instanceId: id,
            instancePath: entry.instancePath,
            historyPath: entry.historyPath,
          });
        }
        const instance = JSON.parse(readFileSync(join(this.config.worktreePath, instancePath), "utf8"));
        const historyFile = JSON.parse(readFileSync(join(this.config.worktreePath, historyPath), "utf8")) as { history?: unknown };
        const normalized = normalizeTodoSnapshotEntry({ instance, history: historyFile.history, source: entry.source });
        if (seenInstanceIds.has(normalized.instance.id)) throw new GitStorageError("Todo Git index contains a duplicate instance", { instanceId: normalized.instance.id });
        seenInstanceIds.add(normalized.instance.id);
        return normalized;
      });
      const reminderNotificationsPath = join(this.config.basePath, "reminders", "notifications.json");
      let reminderNotifications: TodoSnapshot["reminderNotifications"] = [];
      if (index.reminderNotificationsPath !== undefined) {
        if (index.reminderNotificationsPath !== reminderNotificationsPath) {
          throw new GitStorageError("Todo Git index points outside its managed reminder notification path", { reminderNotificationsPath: index.reminderNotificationsPath });
        }
        const reminderFile = JSON.parse(readFileSync(join(this.config.worktreePath, reminderNotificationsPath), "utf8")) as { notifications?: unknown };
        if (!Array.isArray(reminderFile.notifications)) throw new GitStorageError("Todo Git reminder notification file is invalid");
        reminderNotifications = reminderFile.notifications.map(normalizeTodoReminderNotification);
      }
      return {
        schemaVersion: 1,
        generatedAt: typeof (index as { generatedAt?: unknown }).generatedAt === "string" ? (index as { generatedAt: string }).generatedAt : new Date().toISOString(),
        instances,
        reminderNotifications,
      };
    });
  }

  async writeSnapshot(snapshot: TodoSnapshot, reason: string): Promise<Record<string, unknown>> {
    if (!this.enabled()) return { ok: true, changed: false, primary: "postgres" };
    if (snapshot.schemaVersion !== 1 || !Array.isArray(snapshot.instances) || !Array.isArray(snapshot.reminderNotifications)) {
      throw new GitStorageError("Todo snapshot has an unsupported schema");
    }
    const normalized = snapshot;
    return await this.enqueue(async () => {
      this.syncWorktree();
      const normalizedHash = snapshotHash(normalized);
      const root = this.path();
      rmSync(root, { recursive: true, force: true });
      mkdirSync(root, { recursive: true });
      const indexEntries = normalized.instances.map((entry) => {
        const id = safeId(entry.instance.id);
        const instancePath = join(this.config.basePath, "instances", `${id}.json`);
        const historyPath = join(this.config.basePath, "history", `${id}.json`);
        this.writeJson(instancePath, entry.instance);
        this.writeJson(historyPath, { schemaVersion: 1, instanceId: id, history: entry.history });
        return {
          id,
          name: entry.instance.name,
          updatedAt: entry.instance.updatedAt,
          historyPointer: entry.instance.historyPointer,
          instancePath,
          historyPath,
          source: entry.source,
        };
      });
      this.writeJson(join(this.config.basePath, ".todo-note", "schema.json"), {
        schemaVersion: 1,
        generatedBy: "unidesk-todo-note",
      });
      const reminderNotificationsPath = join(this.config.basePath, "reminders", "notifications.json");
      this.writeJson(reminderNotificationsPath, {
        schemaVersion: 1,
        notifications: normalized.reminderNotifications,
      });
      this.writeJson(join(this.config.basePath, "indexes", "instances.json"), {
        schemaVersion: 1,
        generatedAt: normalized.generatedAt,
        hash: normalizedHash,
        reminderNotificationsPath,
        instances: indexEntries,
      });
      this.runGit(["add", "--all", "--", this.config.basePath]);
      const diff = this.runGit(["diff", "--cached", "--quiet"], { allowFailure: true });
      if (diff.exitCode === 0) return { ok: true, changed: false, hash: normalizedHash, head: this.headSummary() };
      const commit = this.runGit(["commit", "-m", `${this.config.commitMessagePrefix} ${reason}`], {
        env: {
          GIT_AUTHOR_NAME: this.config.authorName,
          GIT_AUTHOR_EMAIL: this.config.authorEmail,
          GIT_COMMITTER_NAME: this.config.authorName,
          GIT_COMMITTER_EMAIL: this.config.authorEmail,
        },
      });
      if (!commit.ok) throw new GitStorageError("Todo Git commit failed", { stderr: commit.stderr.slice(-2000) });
      for (let attempt = 1; attempt <= 3; attempt += 1) {
        const pushed = this.runGit(["push", "origin", this.config.branch], { allowFailure: true });
        if (pushed.ok) return { ok: true, changed: true, hash: normalizedHash, attempts: attempt, head: this.headSummary() };
        const fetched = this.runGit(["fetch", "origin", this.config.branch], { allowFailure: true });
        if (!fetched.ok) continue;
        const rebased = this.runGit(["rebase", `origin/${this.config.branch}`], { allowFailure: true });
        if (!rebased.ok) {
          this.runGit(["rebase", "--abort"], { allowFailure: true });
          throw new GitStorageError("Todo Git rebase failed", { attempt, stderr: rebased.stderr.slice(-2000) });
        }
      }
      throw new GitStorageError("Todo Git push failed after retries", { branch: this.config.branch });
    });
  }

  async verify(databaseSnapshot: TodoSnapshot): Promise<Record<string, unknown>> {
    if (!this.enabled()) return { ok: true, primary: "postgres", databaseHash: snapshotHash(databaseSnapshot), gitHash: null };
    const gitSnapshot = await this.readSnapshot();
    const databaseHash = snapshotHash(databaseSnapshot);
    const gitHash = gitSnapshot === null ? null : snapshotHash(gitSnapshot);
    return {
      ok: gitHash === databaseHash,
      primary: "github-repo",
      instances: databaseSnapshot.instances.length,
      reminderNotifications: databaseSnapshot.reminderNotifications.length,
      databaseHash,
      gitHash,
      head: this.headSummary(),
    };
  }

  private async enqueue<T>(operation: () => Promise<T>): Promise<T> {
    const next = this.queue.then(operation, operation);
    this.queue = next.then(() => undefined, () => undefined);
    return await next;
  }

  private syncWorktree(): void {
    mkdirSync(dirname(this.config.worktreePath), { recursive: true });
    if (!existsSync(join(this.config.worktreePath, ".git"))) {
      if (existsSync(this.config.worktreePath)) rmSync(this.config.worktreePath, { recursive: true, force: true });
      this.runGit(["clone", this.config.repoSshUrl, this.config.worktreePath], { cwd: dirname(this.config.worktreePath) });
    }
    this.runGit(["remote", "set-url", "origin", this.config.repoSshUrl]);
    const status = this.runGit(["status", "--porcelain"]);
    if (status.stdout.trim()) throw new GitStorageError("Todo Git worktree contains uncommitted changes", { paths: status.stdout.trim().split("\n").slice(0, 20) });
    const fetched = this.runGit(["fetch", "origin", this.config.branch], { allowFailure: true });
    const localBranch = this.runGit(["show-ref", "--verify", `refs/heads/${this.config.branch}`], { allowFailure: true });
    if (fetched.ok) {
      if (localBranch.ok) {
        this.runGit(["checkout", this.config.branch]);
        this.runGit(["rebase", `origin/${this.config.branch}`]);
      } else {
        this.runGit(["checkout", "-b", this.config.branch, `origin/${this.config.branch}`]);
      }
      return;
    }
    const head = this.runGit(["rev-parse", "--verify", "HEAD"], { allowFailure: true });
    if (localBranch.ok) this.runGit(["checkout", this.config.branch]);
    else if (head.ok) this.runGit(["checkout", "-b", this.config.branch]);
    else this.runGit(["symbolic-ref", "HEAD", `refs/heads/${this.config.branch}`]);
  }

  private runGit(args: string[], options: { cwd?: string; allowFailure?: boolean; env?: Record<string, string> } = {}): GitResult {
    const result = spawnSync("git", args, {
      cwd: options.cwd ?? this.config.worktreePath,
      env: {
        ...process.env,
        ...(this.config.sshCommand ? { GIT_SSH_COMMAND: this.config.sshCommand } : {}),
        ...options.env,
      },
      encoding: "utf8",
      maxBuffer: 8 * 1024 * 1024,
    });
    const exitCode = result.status ?? 1;
    const output = {
      ok: exitCode === 0,
      stdout: typeof result.stdout === "string" ? result.stdout : "",
      stderr: typeof result.stderr === "string" ? result.stderr : "",
      exitCode,
    };
    if (!output.ok && options.allowFailure !== true) {
      throw new GitStorageError("Todo Git command failed", {
        command: ["git", ...args].join(" "),
        exitCode,
        stderr: output.stderr.slice(-2000),
      });
    }
    return output;
  }

  private path(...parts: string[]): string {
    return join(this.config.worktreePath, this.config.basePath, ...parts);
  }

  private writeJson(relativePath: string, value: unknown): void {
    const target = join(this.config.worktreePath, relativePath);
    mkdirSync(dirname(target), { recursive: true });
    writeFileSync(target, `${JSON.stringify(value, null, 2)}\n`, "utf8");
  }

  private headSummary(): Record<string, unknown> {
    if (!existsSync(join(this.config.worktreePath, ".git"))) return { present: false };
    const commit = this.runGit(["rev-parse", "HEAD"], { allowFailure: true });
    const branch = this.runGit(["rev-parse", "--abbrev-ref", "HEAD"], { allowFailure: true });
    return {
      present: commit.ok,
      branch: branch.stdout.trim(),
      commit: commit.stdout.trim().slice(0, 40),
      shortCommit: commit.stdout.trim().slice(0, 12),
    };
  }
}
