import { execFile } from 'node:child_process';
import { promisify } from 'node:util';

const execFileAsync = promisify(execFile);

export interface GitExecResult {
  stdout: string;
  stderr: string;
  exitCode: number;
}

export interface GitCommitResult {
  changed: boolean; // false if "nothing to commit"
  sha: string | null;
}

export class GitClient {
  constructor(private cwd: string, private timeoutMs: number = 60_000) {}

  async run(args: string[]): Promise<GitExecResult> {
    try {
      const { stdout, stderr } = await execFileAsync(
        'git',
        ['-C', this.cwd, ...args],
        { timeout: this.timeoutMs, maxBuffer: 10 * 1024 * 1024 }
      );
      return { stdout, stderr, exitCode: 0 };
    } catch (e) {
      const err = e as { stdout?: string; stderr?: string; code?: number };
      return {
        stdout: err.stdout ?? '',
        stderr: err.stderr ?? String(e),
        exitCode: typeof err.code === 'number' ? err.code : -1
      };
    }
  }

  async isRepo(): Promise<boolean> {
    const r = await this.run(['rev-parse', '--git-dir']);
    return r.exitCode === 0;
  }

  async hasRemote(name: string = 'origin'): Promise<boolean> {
    const r = await this.run(['remote', 'get-url', name]);
    return r.exitCode === 0;
  }

  async addAll(): Promise<void> {
    const r = await this.run(['add', '-A']);
    if (r.exitCode !== 0) throw new Error(`git add failed: ${r.stderr}`);
  }

  async commit(message: string): Promise<GitCommitResult> {
    // Detect "nothing to commit" via stderr/stdout rather than treating as a thrown error.
    const r = await this.run(['commit', '-m', message]);
    if (r.exitCode === 0) {
      const sha = await this.run(['rev-parse', 'HEAD']);
      return { changed: true, sha: sha.stdout.trim() };
    }
    const blob = (r.stdout + '\n' + r.stderr).toLowerCase();
    const nothingToCommit = /nothing to commit|nothing added to commit|no changes added to commit/i.test(blob);
    if (nothingToCommit) {
      return { changed: false, sha: null };
    }
    throw new Error(`git commit failed: ${r.stderr || r.stdout}`);
  }

  async hasUpstream(): Promise<boolean> {
    const r = await this.run(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}']);
    return r.exitCode === 0;
  }

  async push(remote: string = 'origin', branch?: string): Promise<void> {
    // If no upstream is configured, set one with -u so subsequent `git push` works.
    const upstream = await this.hasUpstream();
    let args: string[];
    if (!upstream) {
      const targetBranch = branch ?? (await this.currentBranch());
      args = ['push', '-u', remote, targetBranch];
    } else {
      args = branch ? ['push', remote, branch] : ['push', remote];
    }
    const r = await this.run(args);
    if (r.exitCode !== 0) throw new Error(`git push failed: ${r.stderr}`);
  }

  async currentBranch(): Promise<string> {
    const r = await this.run(['rev-parse', '--abbrev-ref', 'HEAD']);
    if (r.exitCode !== 0) throw new Error(`git rev-parse failed: ${r.stderr}`);
    return r.stdout.trim();
  }

  async fetch(remote: string = 'origin'): Promise<GitExecResult> {
    return this.run(['fetch', remote]);
  }

  async rebaseOnto(ref: string): Promise<GitExecResult> {
    return this.run(['rebase', ref]);
  }

  async rebaseAbort(): Promise<GitExecResult> {
    return this.run(['rebase', '--abort']);
  }

  async hasUncommittedChanges(): Promise<boolean> {
    const r = await this.run(['status', '--porcelain']);
    return r.stdout.trim().length > 0;
  }

  /** ref (branch, tag, remote branch) 존재 여부 확인. `git rev-parse --verify`. */
  async refExists(ref: string): Promise<boolean> {
    const r = await this.run(['rev-parse', '--verify', ref]);
    return r.exitCode === 0;
  }

  /** rebase conflict 시 conflict 마킹된 파일 list. `git diff --name-only --diff-filter=U`. */
  async listConflicts(): Promise<string[]> {
    const r = await this.run(['diff', '--name-only', '--diff-filter=U']);
    return r.stdout.split('\n').map((s) => s.trim()).filter((s) => s.length > 0);
  }
}
