// Abstraction ContentSource (spec §5) : DEV = FS local (grav-reference monté RO),
// PROD = cache local alimenté par rsync/SSH. Le reste de l'app ne voit que cette interface.
import { execFile } from 'node:child_process'
import { promisify } from 'node:util'
import { mkdir } from 'node:fs/promises'
import { join, normalize } from 'node:path'
import type { Project } from '~~/types'
import { readAllProjects } from './grav-reader'

const execFileAsync = promisify(execFile)

export interface SyncResult {
  ok: boolean
  summary: string
  syncedAt: string
}

export interface ContentSource {
  /** Racine FS des projets (catégories/<slug>/…) */
  readonly rootDir: string
  listProjects(): Promise<Project[]>
  getProject(id: string): Promise<Project | null>
  /** Chemin absolu d'un média, borné à rootDir (anti path-traversal). */
  resolveMediaPath(projectId: string, filename: string): string | null
  /** Re-synchronise le contenu (no-op en DEV). */
  refresh(): Promise<SyncResult>
  /** Invalide le cache mémoire. */
  invalidate(): void
}

const CACHE_TTL_MS = 30_000

abstract class FsContentSource implements ContentSource {
  private cache: { at: number; projects: Project[] } | null = null

  constructor(public readonly rootDir: string) {}

  async listProjects(): Promise<Project[]> {
    if (this.cache && Date.now() - this.cache.at < CACHE_TTL_MS) {
      return this.cache.projects
    }
    const projects = await readAllProjects(this.rootDir)
    this.cache = { at: Date.now(), projects }
    return projects
  }

  async getProject(id: string): Promise<Project | null> {
    const projects = await this.listProjects()
    return projects.find(p => p.id === id) ?? null
  }

  resolveMediaPath(projectId: string, filename: string): string | null {
    const abs = normalize(join(this.rootDir, projectId, filename))
    if (!abs.startsWith(normalize(this.rootDir) + '/')) return null
    return abs
  }

  invalidate(): void {
    this.cache = null
  }

  abstract refresh(): Promise<SyncResult>
}

/** Mode DEV : lecture directe du Grav local monté en lecture seule. */
export class DevContentSource extends FsContentSource {
  async refresh(): Promise<SyncResult> {
    this.invalidate()
    const projects = await this.listProjects()
    return {
      ok: true,
      summary: `Mode dev : contenu local relu (${projects.length} projets).`,
      syncedAt: new Date().toISOString(),
    }
  }
}

/**
 * Mode PROD : cache local `data/content-cache/` alimenté par rsync/SSH
 * depuis la prod (spec §6). Lecture seule côté prod, incrémental.
 */
export class ProdContentSource extends FsContentSource {
  constructor(
    rootDir: string,
    private readonly remote: string, // ex. user@host:/chemin/user/pages/02.projets/
    private readonly sshKey?: string,
  ) {
    super(rootDir)
  }

  async refresh(): Promise<SyncResult> {
    if (!this.remote) {
      return {
        ok: false,
        summary: 'NUXT_SYNC_REMOTE non configuré : impossible de synchroniser.',
        syncedAt: new Date().toISOString(),
      }
    }
    await mkdir(this.rootDir, { recursive: true })
    const ssh = ['ssh', '-o', 'StrictHostKeyChecking=accept-new', '-o', 'BatchMode=yes']
    if (this.sshKey) ssh.push('-i', this.sshKey)

    const started = Date.now()
    try {
      const { stdout } = await execFileAsync('rsync', [
        '-az',
        '--delete',
        '--itemize-changes',
        '-e', ssh.join(' '),
        this.remote.endsWith('/') ? this.remote : `${this.remote}/`,
        `${this.rootDir}/`,
      ], { timeout: 15 * 60_000 })
      this.invalidate()
      const changed = stdout.split('\n').filter(l => /^[<>ch.]\S+\s/.test(l)).length
      const projects = await this.listProjects()
      return {
        ok: true,
        summary: `${changed} fichier(s) mis à jour en ${Math.round((Date.now() - started) / 1000)} s — ${projects.length} projets.`,
        syncedAt: new Date().toISOString(),
      }
    } catch (err) {
      return {
        ok: false,
        summary: `Échec rsync : ${err instanceof Error ? err.message : String(err)}`,
        syncedAt: new Date().toISOString(),
      }
    }
  }
}

let source: ContentSource | null = null

export function useContentSource(): ContentSource {
  if (source) return source
  const config = useRuntimeConfig()
  source = config.contentSource === 'prod'
    ? new ProdContentSource(join(config.dataDir, 'content-cache'), config.syncRemote, config.syncSshKey || undefined)
    : new DevContentSource(config.contentDir)
  return source
}
