import { exec, execFile } from 'child_process';
import { promisify } from 'util';
import * as fs from 'fs';
import * as path from 'path';
import {
  abgleichMisserfolg,
  alsIsoDatum,
  dateiVorhanden,
  ersetzeDateiSicher,
  inhaltBeanstanden,
  type Abgleichauftrag,
  type Bezugsquelle,
  type GeheimnisBedarf,
  type QuellenAbgleich,
} from './basis';

const execAsync = promisify(exec);
const execFileAsync = promisify(execFile);

/** Ein Bau darf lange dauern (z. B. PyInstaller mit vielen Abhängigkeiten). */
const BAU_ZEITGRENZE_MS = 30 * 60 * 1000;
const GIT_ZEITGRENZE_MS = 10 * 60 * 1000;

/** Name der Bauanleitung, die ein Repository selbst mitbringt. */
export const BAUANLEITUNG_DATEI = 'software-bibliothek.json';

interface Bauanleitung {
  bauBefehl: string;
  ergebnis: string;
  geheimnisse: GeheimnisBedarf[];
}

/**
 * Aufbau der Anleitung, wie sie im Repository steht. Die Feldnamen sind englisch,
 * weil sie ein Vertrag nach außen sind — jedes anzubindende Tool schreibt sie.
 * Beschrieben im Skill „build-secrets" des Repos dh-claude-rules.
 */
interface AnleitungImRepo {
  buildCommand?: string;
  artifact?: string;
  secrets?: Array<{ name?: string; description?: string }>;
}

/**
 * Interne Tools aus einem Git-Repository: Arbeitskopie holen bzw. aktualisieren,
 * bauen und das erzeugte Programm bereitstellen.
 *
 * Angabe der Quelle: nur die Repo-URL, optional mit `#zweig` (z. B. `ssh://…/tool.git#main`).
 * Wie gebaut wird, beschreibt das Repository selbst in `software-bibliothek.json`:
 *
 *   { "buildCommand": "powershell -ExecutionPolicy Bypass -File build_exe.ps1",
 *     "artifact":     "dist/MeinTool.exe" }
 *
 * So bleibt die Bauanleitung dort, wo sie hingehört — beim Tool. In der Bibliothek
 * lassen sich beide Angaben bei Bedarf überschreiben (für fremde Repos, die man
 * nicht ändern kann).
 *
 * Die Arbeitskopie legt die Anwendung selbst an (`<Datenordner>\quellen\<id>`) — es
 * wird nichts vorausgesetzt, was auf dem jeweiligen Rechner schon vorhanden sein müsste.
 *
 * Hinweis zur Sicherheit: Der Bau-Befehl wird auf dem Server mit den Rechten des
 * Dienstbenutzers ausgeführt. Pflegen kann ihn nur, wer lokal am Server arbeitet
 * bzw. wer ins Repository schreiben darf.
 */
export class GitBuildQuelle implements Bezugsquelle {
  async gleicheAb(auftrag: Abgleichauftrag): Promise<QuellenAbgleich> {
    const [repoUrl, gewuenschterZweig] = auftrag.wert.split('#');
    let commit = '';
    try {
      commit = await arbeitskopieAktualisieren(repoUrl.trim(), gewuenschterZweig?.trim() ?? '', auftrag.arbeitsordner, auftrag.gitSshBefehl);
    } catch (fehler) {
      return abgleichMisserfolg(auftrag.aktuelleVersion, gitFehlertext(fehler));
    }

    const veroeffentlichtAm = await commitZeitpunkt(auftrag.arbeitsordner);
    const anleitung = leseBauanleitung(auftrag);
    if (!anleitung.bauBefehl || !anleitung.ergebnis) {
      return abgleichMisserfolg(
        auftrag.aktuelleVersion,
        `Im Repository fehlt die Datei ${BAUANLEITUNG_DATEI} mit „buildCommand" und „artifact" — alternativ beides in der Bibliothek hinterlegen.`
      );
    }

    // Nur bauen, wenn sich seit dem letzten Mal etwas geändert hat.
    const bekannterDateiname = ergebnisDateiname(anleitung.ergebnis);
    if (commit === auftrag.aktuelleVersion && dateiVorhanden(auftrag.zielordner, bekannterDateiname)) {
      return { erfolg: true, geaendert: false, version: commit, dateiname: bekannterDateiname, meldung: 'Bereits aktuell (unveränderter Stand).', veroeffentlichtAm, benoetigteGeheimnisse: anleitung.geheimnisse };
    }

    const fehlende = anleitung.geheimnisse.filter((bedarf) => !auftrag.geheimnisse[bedarf.name]).map((bedarf) => bedarf.name);
    try {
      // Geheimnisse gehen als Umgebungsvariablen in den Bau — das Tool entscheidet selbst,
      // was es daraus macht. Sie landen dadurch nirgends auf der Platte.
      await execAsync(anleitung.bauBefehl, {
        cwd: auftrag.arbeitsordner,
        timeout: BAU_ZEITGRENZE_MS,
        windowsHide: true,
        maxBuffer: 32 * 1024 * 1024,
        env: { ...process.env, ...auftrag.geheimnisse },
      });
    } catch (fehler) {
      const hinweis = fehlende.length > 0 ? ` Nicht hinterlegte Schlüssel: ${fehlende.join(', ')}.` : '';
      return abgleichMisserfolg(auftrag.aktuelleVersion, `Bau fehlgeschlagen: ${kurzeFehlermeldung(fehler)}${hinweis}`);
    }

    const ergebnisPfad = findeErgebnis(auftrag.arbeitsordner, anleitung.ergebnis);
    if (!ergebnisPfad) {
      return abgleichMisserfolg(auftrag.aktuelleVersion, `Der Bau lief durch, aber unter „${anleitung.ergebnis}" liegt keine Datei.`);
    }

    const dateiname = path.basename(ergebnisPfad);
    const inhalt = fs.readFileSync(ergebnisPfad);
    const beanstandung = inhaltBeanstanden(inhalt, dateiname, null, auftrag.vorherigeGroesse);
    if (beanstandung) {
      return abgleichMisserfolg(auftrag.aktuelleVersion, beanstandung);
    }

    ersetzeDateiSicher(auftrag.zielordner, dateiname, inhalt);
    return {
      erfolg: true,
      geaendert: true,
      version: commit,
      dateiname,
      meldung: `Aus Git gebaut (Stand ${commit.slice(0, 8)}).`,
      veroeffentlichtAm,
      benoetigteGeheimnisse: anleitung.geheimnisse,
    };
  }
}

/**
 * Bauanleitung ermitteln: bevorzugt aus dem Repository selbst, ersatzweise aus den
 * in der Bibliothek hinterlegten Angaben (die dort gepflegten haben Vorrang, damit
 * sich fremde Repos übersteuern lassen).
 */
function leseBauanleitung(auftrag: Abgleichauftrag): Bauanleitung {
  let ausRepo: AnleitungImRepo = {};
  const anleitungsPfad = path.join(auftrag.arbeitsordner, BAUANLEITUNG_DATEI);
  if (fs.existsSync(anleitungsPfad)) {
    try {
      ausRepo = JSON.parse(fs.readFileSync(anleitungsPfad, 'utf8')) as AnleitungImRepo;
    } catch {
      // Unlesbare Anleitung wie „nicht vorhanden" behandeln — die Angaben aus der
      // Bibliothek können den Bau trotzdem ermöglichen.
    }
  }
  const angeforderte = Array.isArray(ausRepo.secrets) ? ausRepo.secrets : [];
  return {
    bauBefehl: auftrag.bauBefehl.trim() || (ausRepo.buildCommand ?? '').trim(),
    ergebnis: auftrag.bauErgebnis.trim() || (ausRepo.artifact ?? '').trim(),
    geheimnisse: angeforderte
      .filter((bedarf): bedarf is { name: string; description?: string } => typeof bedarf?.name === 'string' && !!bedarf.name.trim())
      .map((bedarf) => ({ name: bedarf.name.trim(), beschreibung: String(bedarf.description ?? '') })),
  };
}

/** Klont die Arbeitskopie oder bringt sie auf den neuesten Stand. Liefert den Commit. */
async function arbeitskopieAktualisieren(repoUrl: string, gewuenschterZweig: string, arbeitsordner: string, sshBefehl: string): Promise<string> {
  // Die Kennung kommt aus den Einstellungen, nicht aus der URL — so bleibt der
  // Katalogeintrag frei von Konto-Angaben und ein Kontowechsel ist eine Änderung.
  const umgebung = sshBefehl ? { ...process.env, GIT_SSH_COMMAND: sshBefehl } : process.env;
  const git = (argumente: string[], cwd?: string) =>
    execFileAsync('git', argumente, { cwd, timeout: GIT_ZEITGRENZE_MS, windowsHide: true, maxBuffer: 16 * 1024 * 1024, env: umgebung });

  if (!fs.existsSync(path.join(arbeitsordner, '.git'))) {
    fs.rmSync(arbeitsordner, { recursive: true, force: true });
    fs.mkdirSync(path.dirname(arbeitsordner), { recursive: true });
    await git(['clone', repoUrl, arbeitsordner]);
  } else {
    await git(['remote', 'set-url', 'origin', repoUrl], arbeitsordner);
    await git(['fetch', '--prune', 'origin'], arbeitsordner);
  }

  const zweig = gewuenschterZweig || (await ermittleStandardZweig(arbeitsordner));
  // Harter Abgleich auf den Fernstand — lokale Reste aus einem früheren Bau stören sonst.
  await git(['checkout', '-B', zweig, `origin/${zweig}`], arbeitsordner);
  await git(['reset', '--hard', `origin/${zweig}`], arbeitsordner);
  await git(['clean', '-ffdx', '-e', 'node_modules', '-e', '.venv'], arbeitsordner);

  const { stdout } = await git(['rev-parse', 'HEAD'], arbeitsordner);
  return stdout.trim();
}

/** Zeitpunkt des ausgecheckten Commits — das ist der „Stand" des internen Tools. */
async function commitZeitpunkt(arbeitsordner: string): Promise<string> {
  try {
    const { stdout } = await execFileAsync('git', ['log', '-1', '--format=%cI'], {
      cwd: arbeitsordner,
      timeout: 30000,
      windowsHide: true,
    });
    return alsIsoDatum(stdout.trim());
  } catch {
    return '';
  }
}

async function ermittleStandardZweig(arbeitsordner: string): Promise<string> {
  try {
    const { stdout } = await execFileAsync('git', ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], {
      cwd: arbeitsordner,
      timeout: 30000,
      windowsHide: true,
    });
    return stdout.trim().replace(/^origin\//, '') || 'main';
  } catch {
    return 'main'; // Repos ohne gesetzten HEAD-Zeiger
  }
}

/** Löst den Ergebnis-Pfad auf; im Dateinamen ist ein `*` erlaubt. */
function findeErgebnis(arbeitsordner: string, muster: string): string | undefined {
  const vollstaendig = path.resolve(arbeitsordner, muster);
  if (!muster.includes('*')) {
    return fs.existsSync(vollstaendig) ? vollstaendig : undefined;
  }
  const ordner = path.dirname(vollstaendig);
  if (!fs.existsSync(ordner)) {
    return undefined;
  }
  const regel = new RegExp(`^${path.basename(vollstaendig).replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*')}$`, 'i');
  const treffer = fs.readdirSync(ordner).filter((name) => regel.test(name));
  return treffer.length > 0 ? path.join(ordner, treffer[0]) : undefined;
}

function ergebnisDateiname(muster: string): string {
  const name = path.basename(muster);
  return name.includes('*') ? '' : name;
}

function gitFehlertext(fehler: unknown): string {
  const text = kurzeFehlermeldung(fehler);
  if (/authenticat|permission denied|could not read|host key|publickey/i.test(text)) {
    return `Git-Anmeldung fehlgeschlagen. Der Dienstbenutzer braucht Zugriff auf das Repository (SSH-Schlüssel bzw. einmalige Anmeldung im Windows-Anmeldespeicher). Ursprüngliche Meldung: ${text}`;
  }
  return `Git fehlgeschlagen: ${text}`;
}

/** Kommandozeilen-Fehler enthalten oft seitenlange Ausgaben — auf das Wesentliche kürzen. */
function kurzeFehlermeldung(fehler: unknown): string {
  const roh = fehler instanceof Error ? ((fehler as Error & { stderr?: string }).stderr || fehler.message) : String(fehler);
  const zeilen = roh.split('\n').map((zeile) => zeile.trim()).filter(Boolean);
  return zeilen.slice(-4).join(' | ').slice(0, 500);
}
