import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";

import {
  canonicalSnapshot,
  parseManifest,
  parseSnapshot,
  snapshotManifest,
  verifySnapshotManifest,
  type TaskTreeBackupManifest,
  type TaskTreeSnapshot,
} from "./backup-contracts.ts";

export type TaskTreeGitBackupConfig = {
  repoSshUrl: string;
  branch: string;
  basePath: string;
  worktreePath: string;
  target: string;
  sshCommand: string;
  authorName: string;
  authorEmail: string;
  commitMessagePrefix: string;
};

export type PublishedTaskTreeBackup = {
  changed: boolean;
  contentSha256: string;
  gitCommit: string;
  pushAttempts: number;
  manifest: TaskTreeBackupManifest;
};

let gitQueue: Promise<unknown> = Promise.resolve();

export class TaskTreeGitBackup {
  readonly config: TaskTreeGitBackupConfig;

  constructor(config: TaskTreeGitBackupConfig) {
    this.config = {
      ...config,
      worktreePath: resolve(config.worktreePath),
    };
  }

  async publish(snapshot: TaskTreeSnapshot, reason: string): Promise<PublishedTaskTreeBackup> {
    return enqueueGit(async () => {
      this.ensureWorktree();
      const manifest = snapshotManifest(snapshot, { target: this.config.target });
      const current = this.readCurrent(false);
      if (current?.manifest.contentSha256 === manifest.contentSha256) {
        return {
          changed: false,
          contentSha256: manifest.contentSha256,
          gitCommit: this.head(),
          pushAttempts: 0,
          manifest: current.manifest,
        };
      }
      const base = this.basePath();
      const snapshotPath = join(this.config.worktreePath, base, "snapshot.json");
      const manifestPath = join(this.config.worktreePath, base, "manifest.json");
      mkdirSync(dirname(snapshotPath), { recursive: true });
      writeFileSync(snapshotPath, canonicalSnapshot(snapshot), "utf8");
      writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
      this.git(["add", base]);
      const diff = this.git(["diff", "--cached", "--quiet"], true);
      if (diff.exitCode === 0) {
        return {
          changed: false,
          contentSha256: manifest.contentSha256,
          gitCommit: this.head(),
          pushAttempts: 0,
          manifest,
        };
      }
      const 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,
      };
      this.git(["commit", "-m", `${this.config.commitMessagePrefix} ${reason}`], false, env);
      let pushAttempts = 0;
      for (let attempt = 1; attempt <= 3; attempt += 1) {
        pushAttempts = attempt;
        if (this.git(["push", "origin", this.config.branch], true).ok) {
          return {
            changed: true,
            contentSha256: manifest.contentSha256,
            gitCommit: this.head(),
            pushAttempts,
            manifest,
          };
        }
        if (!this.git(["fetch", "origin", this.config.branch], true).ok) continue;
        const rebase = this.git(["rebase", `origin/${this.config.branch}`], true);
        if (!rebase.ok) {
          this.git(["rebase", "--abort"], true);
          throw codedError("backup_git_rebase_failed", "TaskTree backup Git rebase failed");
        }
      }
      throw codedError("backup_git_push_failed", "TaskTree backup Git push failed after retries");
    });
  }

  readCurrent(sync = true): { snapshot: TaskTreeSnapshot; manifest: TaskTreeBackupManifest; gitCommit: string } | null {
    if (sync) this.ensureWorktree();
    const base = this.basePath();
    const snapshotPath = join(this.config.worktreePath, base, "snapshot.json");
    const manifestPath = join(this.config.worktreePath, base, "manifest.json");
    if (!existsSync(snapshotPath) || !existsSync(manifestPath)) return null;
    const snapshot = parseSnapshot(readFileSync(snapshotPath, "utf8"));
    const manifest = parseManifest(readFileSync(manifestPath, "utf8"));
    if (manifest.target !== this.config.target) throw codedError("backup_target_mismatch", "backup manifest target does not match runtime target");
    verifySnapshotManifest(snapshot, manifest);
    return { snapshot, manifest, gitCommit: this.head() };
  }

  private ensureWorktree(): void {
    if (!this.config.repoSshUrl) throw codedError("backup_repo_missing", "TaskTree backup Git repository is not configured");
    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.git(["clone", this.config.repoSshUrl, this.config.worktreePath], false, {}, dirname(this.config.worktreePath));
    }
    this.git(["remote", "set-url", "origin", this.config.repoSshUrl]);
    const fetched = this.git(["fetch", "origin", this.config.branch], true);
    if (fetched.ok) {
      this.git(["checkout", "-B", this.config.branch, `origin/${this.config.branch}`]);
      this.git(["pull", "--ff-only", "origin", this.config.branch]);
      return;
    }
    if (this.git(["rev-parse", "--verify", "HEAD"], true).ok) this.git(["checkout", "-B", this.config.branch]);
    else this.git(["symbolic-ref", "HEAD", `refs/heads/${this.config.branch}`]);
  }

  private basePath(): string {
    const raw = this.config.basePath.trim().replace(/^\/+/u, "").replace(/\/+$/u, "");
    if (!raw || raw === "." || raw.split("/").some((part) => part === "..")) {
      throw codedError("backup_base_path_invalid", "TaskTree backup base path must be a non-root relative path");
    }
    return raw;
  }

  private head(): string {
    return this.git(["rev-parse", "HEAD"]).stdout.trim();
  }

  private git(
    args: string[],
    allowFailure = false,
    extraEnv: Record<string, string> = {},
    cwd = this.config.worktreePath,
  ): { ok: boolean; exitCode: number; stdout: string; stderr: string } {
    const env = { ...process.env, ...extraEnv };
    if (this.config.sshCommand) env.GIT_SSH_COMMAND = this.config.sshCommand;
    const result = Bun.spawnSync(["git", ...args], { cwd, env, stdout: "pipe", stderr: "pipe" });
    const output = {
      ok: result.exitCode === 0,
      exitCode: result.exitCode,
      stdout: new TextDecoder().decode(result.stdout),
      stderr: new TextDecoder().decode(result.stderr),
    };
    if (!output.ok && !allowFailure) {
      throw codedError("backup_git_command_failed", `TaskTree backup Git command failed: git ${args.join(" ")}: ${output.stderr.slice(-1000)}`);
    }
    return output;
  }
}

function enqueueGit<T>(operation: () => Promise<T>): Promise<T> {
  const next = gitQueue.then(operation, operation);
  gitQueue = next.catch(() => undefined);
  return next;
}

function codedError(code: string, message: string): Error {
  return Object.assign(new Error(message), { code });
}
