import { execFile, spawn } from "node:child_process";
import { mkdir } from "node:fs/promises";
import { promisify } from "node:util";
import type { ChangeMeta, ChangeDetail, TreeEntry } from "@pijulab/shared";

const execFileAsync = promisify(execFile);

export class PijulRepo {
  private constructor(private absPath: string) {}

  static async init(absPath: string): Promise<PijulRepo> {
    await mkdir(absPath, { recursive: true });
    await execFileAsync("pijul", ["init"], { cwd: absPath });
    return new PijulRepo(absPath);
  }

  static open(absPath: string): PijulRepo {
    return new PijulRepo(absPath);
  }

  async listChannels(): Promise<string[]> {
    const { stdout } = await execFileAsync("pijul", ["channel", "list"], {
      cwd: this.absPath,
    });
    return stdout
      .split("\n")
      .map((line) => line.replace(/^\*?\s*/, "").trim())
      .filter(Boolean);
  }

  async listTree(channel: string, subpath = ""): Promise<TreeEntry[]> {
    const args = ["ls", "--channel", channel];
    if (subpath) args.push(subpath);
    let stdout: string;
    try {
      ({ stdout } = await execFileAsync("pijul", args, { cwd: this.absPath }));
    } catch {
      return [];
    }
    return stdout
      .split("\n")
      .filter(Boolean)
      .map((line) => {
        const isDir = line.endsWith("/");
        const name = isDir ? line.slice(0, -1) : line;
        const path = subpath ? `${subpath}/${name}` : name;
        return { name, kind: isDir ? ("dir" as const) : ("file" as const), path };
      });
  }

  readFile(channel: string, filePath: string): Promise<Buffer> {
    return new Promise((resolve, reject) => {
      const chunks: Buffer[] = [];
      const proc = spawn("pijul", ["cat", "--channel", channel, filePath], {
        cwd: this.absPath,
      });
      proc.stdout.on("data", (chunk: Buffer) => chunks.push(Buffer.from(chunk)));
      proc.on("close", (code) => {
        if (code !== 0) reject(new Error(`pijul cat exited with ${code}`));
        else resolve(Buffer.concat(chunks));
      });
      proc.on("error", reject);
    });
  }

  async log(channel: string, limit = 50): Promise<ChangeMeta[]> {
    let stdout: string;
    try {
      ({ stdout } = await execFileAsync(
        "pijul",
        ["log", "--channel", channel, "--output-format", "json", "--limit", String(limit)],
        { cwd: this.absPath },
      ));
    } catch {
      return [];
    }
    const text = stdout.trim();
    if (!text) return [];
    // pijul may output a JSON array or NDJSON
    if (text.startsWith("[")) {
      return JSON.parse(text) as ChangeMeta[];
    }
    return text
      .split("\n")
      .filter(Boolean)
      .map((line) => JSON.parse(line) as ChangeMeta);
  }

  async getChange(hash: string): Promise<ChangeDetail | null> {
    let stdout: string;
    try {
      ({ stdout } = await execFileAsync("pijul", ["change", hash], {
        cwd: this.absPath,
      }));
    } catch {
      return null;
    }
    // pijul change outputs a human-readable format; capture raw diff
    const lines = stdout.split("\n");
    let message = "";
    const authors: Array<{ name: string; email?: string }> = [];
    let timestamp = "";
    const diffLines: string[] = [];
    let headerDone = false;
    for (const line of lines) {
      if (!headerDone) {
        if (line.startsWith("Author:")) {
          authors.push({ name: line.replace("Author:", "").trim() });
        } else if (line.startsWith("Date:")) {
          timestamp = line.replace("Date:", "").trim();
        } else if (line.startsWith("Hash:") || line.startsWith("Change:")) {
          // skip
        } else if (line.trim() === "") {
          headerDone = true;
        }
      } else {
        if (!message) {
          message = line.trim();
        } else {
          diffLines.push(line);
        }
      }
    }
    return { hash, message, authors, timestamp, diff: diffLines.join("\n") };
  }
}