/**
 * Git-based backup service.
 *
 * DB is always source of truth. Git is a downstream replica.
 * The ONLY path from git → DB is explicit restore or importing manual commits.
 *
 * Manual commits (not by mcpd) are detected and imported if they don't conflict
 * with pending DB changes. Conflicts are resolved in favor of the DB.
 *
 * Configuration is stored in a secret named "backup-ssh" in the DB.
 * The secret data contains: repoUrl, publicKey, privateKey.
 * If the secret exists with a repoUrl, backup is enabled.
 * SSH keys are auto-generated if missing from the secret.
 */
import { execFile as execFileCb } from 'child_process';
import { promisify } from 'util';
import { mkdir, readFile, writeFile, unlink, readdir, access } from 'fs/promises';
import { join, dirname } from 'path';
import yaml from 'js-yaml';
import type { PrismaClient } from '@prisma/client';
import {
  serializeAll, resourceToYaml, resourcePath, parseResourcePath,
  BACKUP_KINDS, APPLY_ORDER, type BackupKind,
} from './yaml-serializer.js';

const execFile = promisify(execFileCb);

const BACKUP_DIR = process.env.MCPD_BACKUP_DIR ?? '/data/backup';
const REPO_DIR = join(BACKUP_DIR, 'repo');
const SSH_KEY_PATH = join(BACKUP_DIR, 'id_ed25519');
const MCPD_EMAIL = 'mcpd@mcpctl.local';
const SYNC_INTERVAL_MS = 30_000;
const BACKUP_SECRET_NAME = 'backup-ssh';

export interface BackupStatus {
  enabled: boolean;
  repoUrl: string | null;
  publicKey: string | null;
  gitReachable: boolean;
  lastSyncAt: string | null;
  lastPushAt: string | null;
  lastError: string | null;
  pendingCount: number;
}

export interface BackupLogEntry {
  hash: string;
  date: string;
  author: string;
  message: string;
  manual: boolean; // true if not committed by mcpd
}

export interface RestorePreview {
  targetCommit: string;
  targetDate: string;
  targetMessage: string;
  added: string[];
  removed: string[];
  modified: string[];
}

/** Callback to apply a parsed YAML resource to the DB. */
export type ImportResourceFn = (kind: BackupKind, name: string, doc: Record<string, unknown>) => Promise<void>;
/** Callback to delete a resource from the DB. */
export type DeleteResourceFn = (kind: BackupKind, name: string) => Promise<void>;

export class GitBackupService {
  private repoUrl: string | null = null;
  private publicKey: string | null = null;
  private initialized = false;
  private gitReachable = false;
  private lastSyncAt: Date | null = null;
  private lastPushAt: Date | null = null;
  private lastError: string | null = null;
  private syncTimer: ReturnType<typeof setInterval> | null = null;
  private syncing = false;

  private importResource: ImportResourceFn | null = null;
  private deleteResource: DeleteResourceFn | null = null;

  constructor(
    private readonly prisma: PrismaClient,
  ) {}

  get enabled(): boolean {
    return this.repoUrl !== null;
  }

  /** Set callbacks for importing/deleting resources (called from main.ts after services are ready). */
  setCallbacks(importFn: ImportResourceFn, deleteFn: DeleteResourceFn): void {
    this.importResource = importFn;
    this.deleteResource = deleteFn;
  }

  /** Initialize: read config from backup-ssh secret, setup SSH key, clone/init repo. */
  async init(): Promise<void> {
    // Read config from the backup-ssh secret in the DB
    await this.loadConfigFromSecret();

    if (!this.enabled) {
      console.log('[git-backup] Disabled (no backup-ssh secret with repoUrl)');
      return;
    }

    console.log(`[git-backup] Initializing with repo: ${this.repoUrl}`);

    try {
      await mkdir(BACKUP_DIR, { recursive: true });
      await this.ensureSshKey();
      await this.initRepo();
      await this.initialSync();
      this.initialized = true;
      console.log('[git-backup] Initialized successfully');
    } catch (err) {
      this.lastError = String(err);
      console.error(`[git-backup] Init failed (will retry in sync loop): ${err}`);
      // Don't throw — mcpd should still start even if git is unavailable
    }

    this.startSyncLoop();
  }

  /** Stop the background sync loop. */
  stop(): void {
    if (this.syncTimer) {
      clearInterval(this.syncTimer);
      this.syncTimer = null;
    }
  }

  // ── Public API ──

  /** Enqueue a resource change for git sync. Called by service layer after DB mutations. */
  async enqueue(kind: BackupKind, name: string, action: 'create' | 'update' | 'delete', userName: string): Promise<void> {
    if (!this.enabled) return;

    let yamlContent: string | null = null;
    if (action !== 'delete') {
      try {
        yamlContent = await this.serializeResource(kind, name);
      } catch (err) {
        console.error(`[git-backup] Failed to serialize ${kind}/${name}: ${err}`);
        return;
      }
    }

    await this.prisma.backupPending.create({
      data: { resourceKind: kind, resourceName: name, action, userName, yamlContent },
    });
  }

  /** Get current backup status. */
  async getStatus(): Promise<BackupStatus> {
    const pendingCount = this.enabled
      ? await this.prisma.backupPending.count()
      : 0;

    return {
      enabled: this.enabled,
      repoUrl: this.repoUrl,
      publicKey: this.publicKey,
      gitReachable: this.gitReachable,
      lastSyncAt: this.lastSyncAt?.toISOString() ?? null,
      lastPushAt: this.lastPushAt?.toISOString() ?? null,
      lastError: this.lastError,
      pendingCount,
    };
  }

  /** Get commit history. */
  async getLog(limit = 50): Promise<BackupLogEntry[]> {
    if (!this.initialized) return [];
    try {
      const raw = await this.git('log', `--max-count=${limit}`, '--format=%H|%aI|%an <%ae>|%s|%ce');
      if (!raw) return [];
      return raw.split('\n').map((line) => {
        const [hash, date, author, message, committerEmail] = line.split('|');
        return {
          hash: hash!,
          date: date!,
          author: author!,
          message: message!,
          manual: committerEmail !== MCPD_EMAIL,
        };
      });
    } catch {
      return [];
    }
  }

  /** Preview what a restore to a specific commit would change. */
  async previewRestore(commitHash: string): Promise<RestorePreview> {
    const info = await this.git('log', '-1', '--format=%aI|%s', commitHash);
    const [date, message] = info.split('|');

    const diff = await this.git('diff', '--name-status', `${commitHash}..HEAD`);
    const added: string[] = [];
    const removed: string[] = [];
    const modified: string[] = [];

    for (const line of diff.split('\n')) {
      if (!line) continue;
      const [status, file] = line.split('\t');
      if (!file || !parseResourcePath(file)) continue;
      // Note: status is relative to commitHash→HEAD, so we invert for restore
      if (status === 'A') removed.push(file); // file was added since target → restore removes it
      else if (status === 'D') added.push(file); // file was deleted since target → restore adds it
      else if (status === 'M') modified.push(file);
    }

    return { targetCommit: commitHash, targetDate: date!, targetMessage: message!, added, removed, modified };
  }

  /** Restore DB to the state at a specific commit. */
  async restoreTo(commitHash: string): Promise<{ branchName: string; applied: number; deleted: number; errors: string[] }> {
    if (!this.importResource || !this.deleteResource) {
      throw new Error('Import/delete callbacks not set');
    }

    // 1. Save current timeline as a branch
    const branchName = `timeline-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}`;
    await this.git('branch', branchName);
    console.log(`[git-backup] Saved current state as branch '${branchName}'`);

    // 2. Reset to target commit
    await this.git('reset', '--hard', commitHash);

    // 3. Read all YAML files from the checkout
    const files = await this.readRepoFiles();
    const errors: string[] = [];

    // 4. Collect what exists in DB now
    const dbFiles = await serializeAll(this.prisma);
    const dbResources = new Set<string>();
    for (const path of dbFiles.keys()) {
      dbResources.add(path);
    }

    // 5. Apply all files from the target commit (in dependency order)
    let applied = 0;
    const repoResources = new Set<string>();

    for (const kind of APPLY_ORDER) {
      for (const [filePath, content] of files) {
        const parsed = parseResourcePath(filePath);
        if (!parsed || parsed.kind !== kind) continue;
        repoResources.add(filePath);

        try {
          const doc = yaml.load(content) as Record<string, unknown>;
          if (!doc || typeof doc !== 'object') continue;
          await this.importResource(kind, parsed.name, doc);
          applied++;
        } catch (err) {
          errors.push(`${filePath}: ${err}`);
        }
      }
    }

    // 6. Delete resources not in the target commit
    let deleted = 0;
    for (const path of dbResources) {
      if (!repoResources.has(path)) {
        const parsed = parseResourcePath(path);
        if (!parsed) continue;
        try {
          await this.deleteResource(parsed.kind, parsed.name);
          deleted++;
        } catch (err) {
          errors.push(`delete ${path}: ${err}`);
        }
      }
    }

    // 7. Clear pending queue (we just wrote everything)
    await this.prisma.backupPending.deleteMany();

    // 8. Commit the restore action
    await this.git('add', '-A');
    const hasChanges = await this.hasUncommittedChanges();
    if (hasChanges) {
      await this.gitCommit(`restore to ${commitHash.slice(0, 7)} (from branch ${branchName})`, 'mcpd');
    }

    // 9. Push
    await this.tryPush();

    return { branchName, applied, deleted, errors };
  }

  // ── Git Operations ──

  private async git(...args: string[]): Promise<string> {
    const env = this.gitEnv();
    const { stdout } = await execFile('git', args, { cwd: REPO_DIR, env, timeout: 30_000 });
    return stdout.trim();
  }

  private async gitCommit(message: string, userName: string): Promise<void> {
    const env = this.gitEnv(userName);
    await execFile('git', ['commit', '-m', message], { cwd: REPO_DIR, env, timeout: 10_000 });
  }

  private gitEnv(authorName?: string): NodeJS.ProcessEnv {
    return {
      ...process.env,
      GIT_SSH_COMMAND: `ssh -i ${SSH_KEY_PATH} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes`,
      GIT_AUTHOR_NAME: authorName ?? 'mcpd',
      GIT_AUTHOR_EMAIL: authorName && authorName !== 'mcpd' ? `${authorName}@mcpctl.local` : MCPD_EMAIL,
      GIT_COMMITTER_NAME: 'mcpd',
      GIT_COMMITTER_EMAIL: MCPD_EMAIL,
    };
  }

  private async hasUncommittedChanges(): Promise<boolean> {
    const status = await this.git('status', '--porcelain');
    return status.length > 0;
  }

  // ── Config from Secret ──

  /** Load repoUrl and SSH keys from the backup-ssh secret. */
  private async loadConfigFromSecret(): Promise<void> {
    try {
      const secret = await this.prisma.secret.findUnique({ where: { name: BACKUP_SECRET_NAME } });
      if (!secret) return;

      const data = secret.data as Record<string, string>;
      const repoUrl = data['repoUrl'];
      if (!repoUrl) return;

      this.repoUrl = repoUrl;
      this.publicKey = data['publicKey'] ?? null;
    } catch (err) {
      console.error(`[git-backup] Failed to read ${BACKUP_SECRET_NAME} secret: ${err}`);
    }
  }

  // ── SSH Key ──

  /** Ensure SSH key exists on disk (for git operations) and in the secret. */
  private async ensureSshKey(): Promise<void> {
    // Try to load key from secret first
    const secret = await this.prisma.secret.findUnique({ where: { name: BACKUP_SECRET_NAME } });
    const data = (secret?.data ?? {}) as Record<string, string>;

    if (data['privateKey'] && data['publicKey']) {
      // Write keys from secret to disk for git SSH
      await writeFile(SSH_KEY_PATH, data['privateKey'] + '\n', { mode: 0o600 });
      await writeFile(`${SSH_KEY_PATH}.pub`, data['publicKey'] + '\n', { mode: 0o644 });
      this.publicKey = data['publicKey'];
      console.log('[git-backup] SSH key loaded from secret');
      return;
    }

    // No keys in secret — check disk (migration from old setup)
    try {
      await access(SSH_KEY_PATH);
      const privateKey = (await readFile(SSH_KEY_PATH, 'utf-8')).trim();
      const pubKey = (await readFile(`${SSH_KEY_PATH}.pub`, 'utf-8')).trim();
      // Store keys back into the secret
      await this.prisma.secret.update({
        where: { name: BACKUP_SECRET_NAME },
        data: { data: { ...data, privateKey, publicKey: pubKey } },
      });
      this.publicKey = pubKey;
      console.log('[git-backup] SSH key migrated from disk to secret');
      return;
    } catch {
      // No key on disk either
    }

    // Generate new keypair
    console.log('[git-backup] Generating SSH key...');
    await execFile('ssh-keygen', ['-t', 'ed25519', '-f', SSH_KEY_PATH, '-N', '', '-C', MCPD_EMAIL], {
      timeout: 10_000,
    });

    const privateKey = (await readFile(SSH_KEY_PATH, 'utf-8')).trim();
    const pubKey = (await readFile(`${SSH_KEY_PATH}.pub`, 'utf-8')).trim();

    // Store in secret
    await this.prisma.secret.update({
      where: { name: BACKUP_SECRET_NAME },
      data: { data: { ...data, privateKey, publicKey: pubKey } },
    });
    this.publicKey = pubKey;
    console.log('[git-backup] SSH key generated and stored in secret');
  }

  // ── Repo Init ──

  private async initRepo(): Promise<void> {
    try {
      await access(join(REPO_DIR, '.git'));
      // Repo exists — ensure remote origin is correct
      try {
        const { stdout: currentUrl } = await execFile('git', ['remote', 'get-url', 'origin'], { cwd: REPO_DIR });
        if (currentUrl.trim() !== this.repoUrl) {
          await execFile('git', ['remote', 'set-url', 'origin', this.repoUrl!], { cwd: REPO_DIR });
          console.log(`[git-backup] Updated remote origin to ${this.repoUrl}`);
        }
      } catch {
        // No remote — add it
        await execFile('git', ['remote', 'add', 'origin', this.repoUrl!], { cwd: REPO_DIR });
        console.log(`[git-backup] Added remote origin: ${this.repoUrl}`);
      }
      console.log('[git-backup] Repo already cloned');
      return;
    } catch {
      // Not cloned yet
    }

    await mkdir(REPO_DIR, { recursive: true });

    try {
      // Try to clone
      const env = this.gitEnv();
      await execFile('git', ['clone', this.repoUrl!, REPO_DIR], { env, timeout: 60_000 });
      this.gitReachable = true;
      console.log('[git-backup] Cloned repo');
    } catch (cloneErr) {
      // Clone failed — maybe empty repo or network issue
      // Init locally, set remote
      console.log(`[git-backup] Clone failed (${cloneErr}), initializing locally`);
      await execFile('git', ['init'], { cwd: REPO_DIR });
      await execFile('git', ['remote', 'add', 'origin', this.repoUrl!], { cwd: REPO_DIR });

      // Create initial commit so we have a branch
      const env = this.gitEnv();
      await writeFile(join(REPO_DIR, '.gitkeep'), '');
      await execFile('git', ['add', '.gitkeep'], { cwd: REPO_DIR, env });
      await execFile('git', ['commit', '-m', 'init'], { cwd: REPO_DIR, env });

      this.gitReachable = false;
    }
  }

  // ── Initial Sync ──

  private async initialSync(): Promise<void> {
    // Check if DB is fresh (no servers, no user-created projects)
    const serverCount = await this.prisma.mcpServer.count();
    const projectCount = await this.prisma.project.count();
    const isFreshDb = serverCount === 0 && projectCount <= 1; // 1 = system project only

    if (isFreshDb) {
      // Fresh DB — try to restore from git
      const files = await this.readRepoFiles();
      if (files.size > 0 && this.importResource) {
        console.log(`[git-backup] Fresh DB, restoring ${files.size} files from git...`);
        await this.importFromFiles(files);
        return;
      }
    }

    // Existing DB — full resync (DB → git)
    await this.fullResync();
  }

  /** Dump all DB resources to git, commit any changes. */
  private async fullResync(): Promise<void> {
    const files = await serializeAll(this.prisma);
    let changed = false;

    // Write all files
    for (const [filePath, content] of files) {
      const fullPath = join(REPO_DIR, filePath);
      await mkdir(dirname(fullPath), { recursive: true });

      let existing: string | null = null;
      try {
        existing = await readFile(fullPath, 'utf-8');
      } catch { /* doesn't exist */ }

      if (existing !== content + '\n') {
        await writeFile(fullPath, content + '\n');
        changed = true;
      }
    }

    // Remove files not in DB
    for (const kind of BACKUP_KINDS) {
      const dir = kind === 'rbac' ? 'rbac' : `${kind}s`;
      const dirPath = join(REPO_DIR, dir);
      try {
        const entries = await readdir(dirPath);
        for (const entry of entries) {
          if (!entry.endsWith('.yaml')) continue;
          const filePath = `${dir}/${entry}`;
          if (!files.has(filePath)) {
            await unlink(join(REPO_DIR, filePath));
            changed = true;
          }
        }
      } catch { /* dir doesn't exist */ }
    }

    if (changed) {
      await this.git('add', '-A');
      if (await this.hasUncommittedChanges()) {
        await this.gitCommit('sync: full resync from database', 'mcpd');
      }
    }

    this.lastSyncAt = new Date();
  }

  // ── Sync Loop ──

  private startSyncLoop(): void {
    this.syncTimer = setInterval(() => {
      if (!this.syncing) {
        this.syncCycle().catch((err) => {
          console.error(`[git-backup] Sync cycle error: ${err}`);
        });
      }
    }, SYNC_INTERVAL_MS);
  }

  /** One sync cycle: fetch → import manual → process pending → push. */
  private async syncCycle(): Promise<void> {
    this.syncing = true;
    try {
      // 1. Fetch remote (detect connectivity)
      const canFetch = await this.tryFetch();

      // 2. Import manual commits (if remote is reachable)
      if (canFetch) {
        await this.importManualCommits();
        // Merge remote into local
        try {
          await this.git('merge', 'origin/main', '--no-edit');
        } catch {
          // Merge conflict — resolve in favor of ours
          try {
            await this.git('checkout', '--ours', '.');
            await this.git('add', '-A');
            await this.gitCommit('merge: resolve conflict (DB wins)', 'mcpd');
          } catch { /* no conflict files */ }
        }
      }

      // 3. Process pending queue
      await this.processPendingQueue();

      // 4. Push
      if (canFetch) {
        await this.tryPush();
      }

      this.lastSyncAt = new Date();
      if (this.lastError && canFetch) {
        console.log('[git-backup] Reconnected, sync restored');
        this.lastError = null;
      }
    } finally {
      this.syncing = false;
    }
  }

  private async tryFetch(): Promise<boolean> {
    try {
      await this.git('fetch', 'origin');
      this.gitReachable = true;
      return true;
    } catch (err) {
      this.gitReachable = false;
      this.lastError = `fetch failed: ${err}`;
      return false;
    }
  }

  private async tryPush(): Promise<boolean> {
    try {
      await this.git('push', 'origin', 'HEAD');
      this.lastPushAt = new Date();
      this.gitReachable = true;
      return true;
    } catch (err) {
      this.lastError = `push failed: ${err}`;
      return false;
    }
  }

  /** Find and import commits on remote that were NOT made by mcpd. */
  private async importManualCommits(): Promise<void> {
    if (!this.importResource) return;

    try {
      // Find commits on remote not yet merged locally
      const raw = await this.git('log', 'HEAD..origin/main', '--format=%H|%ce', '--reverse');
      if (!raw) return;

      // Get pending resource keys for conflict detection
      const pending = await this.prisma.backupPending.findMany({
        select: { resourceKind: true, resourceName: true },
      });
      const pendingKeys = new Set(pending.map((p) => `${p.resourceKind}/${p.resourceName}`));

      for (const line of raw.split('\n')) {
        if (!line) continue;
        const [hash, committerEmail] = line.split('|');
        if (committerEmail === MCPD_EMAIL) continue; // Skip mcpd's own commits

        console.log(`[git-backup] Detected manual commit: ${hash!.slice(0, 7)}`);

        // Get files changed in this commit
        const diff = await this.git('diff-tree', '--no-commit-id', '-r', '--name-status', hash!);
        for (const diffLine of diff.split('\n')) {
          if (!diffLine) continue;
          const parts = diffLine.split('\t');
          const statusChar = parts[0]!;
          const filePath = parts[parts.length - 1]!; // Handle renames: last element is the target

          const parsed = parseResourcePath(filePath);
          if (!parsed) continue;

          const key = `${parsed.kind}/${parsed.name}`;
          if (pendingKeys.has(key)) {
            console.log(`[git-backup] Conflict for ${key} — DB wins, skipping manual change`);
            continue;
          }

          if (statusChar === 'D') {
            // Manual deletion
            try {
              await this.deleteResource!(parsed.kind, parsed.name);
              console.log(`[git-backup] Imported manual delete: ${key}`);
            } catch (err) {
              console.error(`[git-backup] Failed to import delete ${key}: ${err}`);
            }
          } else {
            // Manual add/modify — read file content from that commit
            try {
              const content = await this.git('show', `${hash}:${filePath}`);
              const doc = yaml.load(content) as Record<string, unknown>;
              if (doc && typeof doc === 'object') {
                await this.importResource!(parsed.kind, parsed.name, doc);
                console.log(`[git-backup] Imported manual change: ${key}`);
              }
            } catch (err) {
              console.error(`[git-backup] Failed to import ${key}: ${err}`);
            }
          }
        }
      }
    } catch (err) {
      console.error(`[git-backup] Error importing manual commits: ${err}`);
    }
  }

  /** Process pending queue: write YAML files, commit each change, clear queue. */
  private async processPendingQueue(): Promise<void> {
    const entries = await this.prisma.backupPending.findMany({
      orderBy: { createdAt: 'asc' },
    });
    if (entries.length === 0) return;

    for (const entry of entries) {
      const filePath = resourcePath(entry.resourceKind as BackupKind, entry.resourceName);
      const fullPath = join(REPO_DIR, filePath);

      try {
        if (entry.action === 'delete') {
          try {
            await unlink(fullPath);
          } catch { /* file may not exist */ }
        } else {
          await mkdir(dirname(fullPath), { recursive: true });
          await writeFile(fullPath, (entry.yamlContent ?? '') + '\n');
        }

        await this.git('add', '-A');
        if (await this.hasUncommittedChanges()) {
          const message = `${entry.action} ${entry.resourceKind}/${entry.resourceName} (user: ${entry.userName})`;
          await this.gitCommit(message, entry.userName);
        }

        // Remove processed entry
        await this.prisma.backupPending.delete({ where: { id: entry.id } });
      } catch (err) {
        console.error(`[git-backup] Failed to process pending ${entry.resourceKind}/${entry.resourceName}: ${err}`);
        // Don't delete — will retry next cycle
        break; // Stop processing to maintain order
      }
    }
  }

  // ── Helpers ──

  /** Serialize a single resource to YAML by querying the DB. */
  private async serializeResource(kind: BackupKind, name: string): Promise<string> {
    switch (kind) {
      case 'server': {
        const r = await this.prisma.mcpServer.findUnique({ where: { name } });
        if (!r) throw new Error(`Server not found: ${name}`);
        return resourceToYaml('server', r as unknown as Record<string, unknown>);
      }
      case 'secret': {
        const r = await this.prisma.secret.findUnique({ where: { name } });
        if (!r) throw new Error(`Secret not found: ${name}`);
        return resourceToYaml('secret', r as unknown as Record<string, unknown>);
      }
      case 'project': {
        const r = await this.prisma.project.findUnique({
          where: { name },
          include: { servers: { include: { server: { select: { name: true } } } } },
        });
        if (!r) throw new Error(`Project not found: ${name}`);
        return resourceToYaml('project', r as unknown as Record<string, unknown>);
      }
      case 'user': {
        const r = await this.prisma.user.findUnique({ where: { email: name } });
        if (!r) throw new Error(`User not found: ${name}`);
        return resourceToYaml('user', r as unknown as Record<string, unknown>);
      }
      case 'group': {
        const r = await this.prisma.group.findUnique({
          where: { name },
          include: { members: { include: { user: { select: { email: true } } } } },
        });
        if (!r) throw new Error(`Group not found: ${name}`);
        return resourceToYaml('group', r as unknown as Record<string, unknown>);
      }
      case 'rbac': {
        const r = await this.prisma.rbacDefinition.findUnique({ where: { name } });
        if (!r) throw new Error(`RBAC definition not found: ${name}`);
        return resourceToYaml('rbac', r as unknown as Record<string, unknown>);
      }
      case 'prompt': {
        const r = await this.prisma.prompt.findFirst({
          where: { name },
          include: { project: { select: { name: true } } },
        });
        if (!r) throw new Error(`Prompt not found: ${name}`);
        return resourceToYaml('prompt', r as unknown as Record<string, unknown>);
      }
      case 'template': {
        const r = await this.prisma.mcpTemplate.findUnique({ where: { name } });
        if (!r) throw new Error(`Template not found: ${name}`);
        return resourceToYaml('template', r as unknown as Record<string, unknown>);
      }
      default:
        throw new Error(`Unknown resource kind: ${kind}`);
    }
  }

  /** Read all YAML files from the repo checkout. */
  private async readRepoFiles(): Promise<Map<string, string>> {
    const files = new Map<string, string>();

    for (const kind of BACKUP_KINDS) {
      const dir = kind === 'rbac' ? 'rbac' : `${kind}s`;
      const dirPath = join(REPO_DIR, dir);
      try {
        const entries = await readdir(dirPath);
        for (const entry of entries) {
          if (!entry.endsWith('.yaml')) continue;
          const filePath = `${dir}/${entry}`;
          const content = await readFile(join(REPO_DIR, filePath), 'utf-8');
          files.set(filePath, content);
        }
      } catch { /* dir doesn't exist */ }
    }

    return files;
  }

  /** Import all files from the repo into the DB. */
  private async importFromFiles(files: Map<string, string>): Promise<void> {
    if (!this.importResource) return;

    for (const kind of APPLY_ORDER) {
      for (const [filePath, content] of files) {
        const parsed = parseResourcePath(filePath);
        if (!parsed || parsed.kind !== kind) continue;

        try {
          const doc = yaml.load(content) as Record<string, unknown>;
          if (doc && typeof doc === 'object') {
            await this.importResource(kind, parsed.name, doc);
          }
        } catch (err) {
          console.error(`[git-backup] Failed to import ${filePath}: ${err}`);
        }
      }
    }
  }
}
