/**
 * cli:cross-validate-stack — execute.ts
 * 5 checks de coherence cross-stack backend .NET / frontend React
 * Ne fait PAS de validation d'input — c'est le role de validate.ts
 */
import { readFileSync, readdirSync, statSync } from 'node:fs'
import { join, relative } from 'node:path'
import type {
  CrossValidateInput,
  CrossValidateReport,
  CheckResult,
  SpecEndpoints,
  SpecMeta,
  SpecPermissions,
} from './types'

// ─── Utilitaires ───

/** Parcours recursif pour trouver des fichiers par extension */
function findFiles(dir: string, extension: string): string[] {
  const results: string[] = []
  let entries: string[]
  try {
    entries = readdirSync(dir)
  } catch {
    return results
  }
  for (const entry of entries) {
    const fullPath = join(dir, entry)
    let stat
    try {
      stat = statSync(fullPath)
    } catch {
      continue
    }
    if (stat.isDirectory()) {
      if (entry === 'bin' || entry === 'obj' || entry === 'node_modules' || entry === '.git') continue
      results.push(...findFiles(fullPath, extension))
    } else if (entry.endsWith(extension)) {
      results.push(fullPath)
    }
  }
  return results
}

/** Parcours recursif pour trouver des fichiers par pattern regex sur le nom */
function findFilesByPattern(dir: string, pattern: RegExp): string[] {
  const results: string[] = []
  let entries: string[]
  try {
    entries = readdirSync(dir)
  } catch {
    return results
  }
  for (const entry of entries) {
    const fullPath = join(dir, entry)
    let stat
    try {
      stat = statSync(fullPath)
    } catch {
      continue
    }
    if (stat.isDirectory()) {
      if (entry === 'bin' || entry === 'obj' || entry === 'node_modules' || entry === '.git') continue
      results.push(...findFilesByPattern(fullPath, pattern))
    } else if (pattern.test(entry)) {
      results.push(fullPath)
    }
  }
  return results
}

/** Trouve tous les fichiers JSON nommes exactement filename dans un repertoire recursif */
function findJsonFiles(dir: string, filename: string): string[] {
  const results: string[] = []
  let entries: string[]
  try {
    entries = readdirSync(dir)
  } catch {
    return results
  }
  for (const entry of entries) {
    const fullPath = join(dir, entry)
    let stat
    try {
      stat = statSync(fullPath)
    } catch {
      continue
    }
    if (stat.isDirectory()) {
      results.push(...findJsonFiles(fullPath, filename))
    } else if (entry === filename) {
      results.push(fullPath)
    }
  }
  return results
}

/** Lit et parse un fichier JSON, retourne null si echec */
function readJson<T>(filePath: string): T | null {
  try {
    const content = readFileSync(filePath, 'utf-8')
    return JSON.parse(content) as T
  } catch {
    return null
  }
}

/** Normalise une route en supprimant les contraintes de type (ex: {id:guid} -> {id}) */
function normalizeRoute(route: string): string {
  return route.replace(/\{(\w+):[^}]+\}/g, '{$1}')
}

/** Lit le contenu d'un fichier en string */
function readFile(filePath: string): string {
  try {
    return readFileSync(filePath, 'utf-8')
  } catch {
    return ''
  }
}

// ─── Structures internes ───

interface ResourceSpec {
  /** Chemin relatif du dossier spec (ex: crm/clients) */
  path: string
  meta: SpecMeta
  endpoints: SpecEndpoints | null
  permissions: SpecPermissions | null
}

/** Charge toutes les specs depuis le repertoire specs */
function loadAllSpecs(specsDir: string): ResourceSpec[] {
  const metaFiles = findJsonFiles(specsDir, 'meta.json')
  const resources: ResourceSpec[] = []

  for (const metaFile of metaFiles) {
    const meta = readJson<SpecMeta>(metaFile)
    if (!meta) continue

    // Le dossier parent du meta.json contient les autres fichiers
    const specDir = join(metaFile, '..')
    const relPath = relative(specsDir, specDir)

    const endpoints = readJson<SpecEndpoints>(join(specDir, 'endpoints.json'))
    const permissions = readJson<SpecPermissions>(join(specDir, 'permissions.json'))

    resources.push({ path: relPath, meta, endpoints, permissions })
  }

  return resources
}

// ─── Check 1: routes-have-endpoints ───
// Verifie que chaque endpoint specifie dans les specs a un controller .NET correspondant

function checkRoutesHaveEndpoints(
  resources: ResourceSpec[],
  csFiles: string[],
  srcDir: string,
): CheckResult {
  if (resources.length === 0) {
    return { id: 'routes-have-endpoints', status: 'skipped', details: 'No specs found' }
  }

  // Extraire les routes exposees par les controllers .cs
  // Pattern: [HttpGet], [HttpPost], [HttpPut], [HttpDelete], [HttpPatch]
  const controllerRoutes = new Map<string, Set<string>>()

  for (const file of csFiles) {
    const content = readFile(file)
    // Trouver le NavRoute pour identifier la ressource
    const navMatch = content.match(/\[NavRoute\("([^"]+)"\)\]/)
    if (!navMatch) continue
    const navRoute = navMatch[1]

    // Extraire les verbes HTTP
    const httpRegex = /\[Http(Get|Post|Put|Delete|Patch)(?:\("([^"]*)"\))?\]/g
    let match
    const verbs = new Set<string>()
    while ((match = httpRegex.exec(content)) !== null) {
      const verb = match[1].toUpperCase()
      const route = normalizeRoute(match[2] || '')
      verbs.add(`${verb}:${route}`)
    }
    controllerRoutes.set(navRoute, verbs)
  }

  const missing: string[] = []
  let matchCount = 0

  for (const res of resources) {
    if (!res.endpoints) continue

    // Construire le navRoute attendu: module.section
    const expectedNavRoute = `${res.meta.module}.${res.meta.section}`

    const controllerVerbs = controllerRoutes.get(expectedNavRoute)

    for (const ep of res.endpoints.endpoints) {
      if (!ep.verb) continue // skip malformed endpoint entries
      const epKey = `${ep.verb.toUpperCase()}:${normalizeRoute(ep.route || '')}`
      if (controllerVerbs && controllerVerbs.has(epKey)) {
        matchCount++
      } else {
        missing.push(`${expectedNavRoute} ${ep.verb} /${ep.route || ''}`)
      }
    }
  }

  if (missing.length > 0) {
    return {
      id: 'routes-have-endpoints',
      status: 'error',
      details: `Missing controllers for: ${missing.join('; ')}`,
    }
  }

  return { id: 'routes-have-endpoints', status: 'ok', count: matchCount }
}

// ─── Check 2: endpoints-are-consumed ───
// Verifie que chaque endpoint .NET est appele par au moins un service frontend

function checkEndpointsAreConsumed(
  csFiles: string[],
  tsFiles: string[],
  srcDir: string,
): CheckResult {
  if (csFiles.length === 0) {
    return { id: 'endpoints-are-consumed', status: 'skipped', details: 'No controller files found' }
  }

  // Extraire les endpoints des controllers .cs
  interface CsEndpoint {
    navRoute: string
    verb: string
    route: string
  }

  const csEndpoints: CsEndpoint[] = []

  for (const file of csFiles) {
    const content = readFile(file)
    const navMatch = content.match(/\[NavRoute\("([^"]+)"\)\]/)
    if (!navMatch) continue
    const navRoute = navMatch[1]

    const httpRegex = /\[Http(Get|Post|Put|Delete|Patch)(?:\("([^"]*)"\))?\]/g
    let match
    while ((match = httpRegex.exec(content)) !== null) {
      csEndpoints.push({
        navRoute,
        verb: match[1].toUpperCase(),
        route: normalizeRoute(match[2] || ''),
      })
    }
  }

  if (csEndpoints.length === 0) {
    return { id: 'endpoints-are-consumed', status: 'skipped', details: 'No endpoints found in controllers' }
  }

  // Lire tout le contenu TS/TSX pour chercher les appels API
  let allTsContent = ''
  for (const file of tsFiles) {
    allTsContent += readFile(file) + '\n'
  }

  if (allTsContent.trim().length === 0) {
    // Pas de fichiers frontend: tout est "non-consomme"
    const uncovered = csEndpoints.map(ep => `${ep.navRoute} ${ep.verb} /${ep.route}`)
    return {
      id: 'endpoints-are-consumed',
      status: 'warning',
      details: `No frontend files found. Uncovered: ${uncovered.join('; ')}`,
    }
  }

  const notConsumed: string[] = []
  let consumedCount = 0

  for (const ep of csEndpoints) {
    // Chercher des patterns courants d'appel API dans le frontend:
    // - fetch/axios/apiFetch avec le verbe (get, post, delete, put, patch)
    // - Le navRoute ou une URL derivee (ex: crm/clients, /api/crm/clients)
    const routeParts = ep.navRoute.split('.')
    const urlPattern = routeParts.join('/')
    const verbLower = ep.verb.toLowerCase()

    // Patterns de recherche: le verbe HTTP ou une URL qui contient la route
    const hasVerbCall = new RegExp(
      `(?:${verbLower}|${ep.verb}).*${urlPattern.replace(/\./g, '\\.')}|${urlPattern.replace(/\./g, '/')}.*(?:${verbLower}|${ep.verb})`,
      'i',
    ).test(allTsContent)

    // Pattern alternatif: juste la presence de la route dans les services
    const hasRouteRef = allTsContent.includes(urlPattern) ||
      allTsContent.includes(urlPattern.replace(/\./g, '/'))

    if (hasVerbCall || hasRouteRef) {
      consumedCount++
    } else {
      notConsumed.push(`${ep.navRoute} ${ep.verb} /${ep.route}`)
    }
  }

  if (notConsumed.length > 0) {
    return {
      id: 'endpoints-are-consumed',
      status: 'warning',
      details: `Endpoints not consumed by frontend: ${notConsumed.join('; ')}`,
    }
  }

  return { id: 'endpoints-are-consumed', status: 'ok', count: consumedCount }
}

// ─── Check 3: permissions-aligned ───
// Verifie que les memes permissions sont utilisees cote .NET et cote React

function checkPermissionsAligned(
  csFiles: string[],
  tsFiles: string[],
  srcDir: string,
): CheckResult {
  // Extraire les permissions .NET: [RequirePermission(Permissions.X.Y)]
  const dotnetPerms = new Set<string>()
  for (const file of csFiles) {
    const content = readFile(file)
    const permRegex = /\[RequirePermission\(([^)]+)\)\]/g
    let match
    while ((match = permRegex.exec(content)) !== null) {
      // Normaliser: Permissions.Clients.Read -> Clients.Read
      const perm = match[1].trim()
      dotnetPerms.add(perm)
    }
  }

  // Extraire les permissions React: PermissionGuard permission="X.Y" / usePermission("X.Y")
  const reactPerms = new Set<string>()
  for (const file of tsFiles) {
    const content = readFile(file)

    // PermissionGuard permission="Permissions.X.Y" ou permission={Permissions.X.Y}
    const guardRegex = /PermissionGuard[^>]*permission[=:]\s*["{]([^"}>]+)/g
    let match
    while ((match = guardRegex.exec(content)) !== null) {
      reactPerms.add(match[1].trim())
    }

    // usePermission("Permissions.X.Y") ou usePermission(Permissions.X.Y)
    const hookRegex = /usePermission\(\s*["']?([^"')]+)["']?\s*\)/g
    while ((match = hookRegex.exec(content)) !== null) {
      reactPerms.add(match[1].trim())
    }
  }

  if (dotnetPerms.size === 0 && reactPerms.size === 0) {
    return { id: 'permissions-aligned', status: 'skipped', details: 'No permissions found in either side' }
  }

  const misaligned: string[] = []

  // Verifier: chaque permission .NET doit exister cote React
  for (const perm of dotnetPerms) {
    if (!reactPerms.has(perm)) {
      misaligned.push(`${perm} — .NET: ok, React: missing`)
    }
  }

  // Verifier: chaque permission React doit exister cote .NET
  for (const perm of reactPerms) {
    if (!dotnetPerms.has(perm)) {
      misaligned.push(`${perm} — React: ok, .NET: missing`)
    }
  }

  if (misaligned.length > 0) {
    return {
      id: 'permissions-aligned',
      status: 'error',
      details: misaligned.join('; '),
    }
  }

  return { id: 'permissions-aligned', status: 'ok', count: dotnetPerms.size }
}

// ─── Check 4: resources-have-screens ───
// Verifie que chaque ressource a un controller .cs ET au moins une page .tsx

function checkResourcesHaveScreens(
  resources: ResourceSpec[],
  csFiles: string[],
  tsxFiles: string[],
  srcDir: string,
): CheckResult {
  if (resources.length === 0) {
    return { id: 'resources-have-screens', status: 'skipped', details: 'No specs found' }
  }

  // Indexer les controllers par NavRoute
  const controllerNavRoutes = new Set<string>()
  for (const file of csFiles) {
    const content = readFile(file)
    const navMatch = content.match(/\[NavRoute\("([^"]+)"\)\]/)
    if (navMatch) {
      controllerNavRoutes.add(navMatch[1])
    }
  }

  // Indexer les pages .tsx par nom de ressource (heuristique sur le nom de fichier)
  const tsxFilenames = new Set<string>()
  for (const file of tsxFiles) {
    const content = readFile(file)
    // Ajouter le nom du fichier en lowercase
    const parts = file.replace(/\\/g, '/').split('/')
    const filename = parts[parts.length - 1].replace(/\.(tsx|ts)$/, '').toLowerCase()
    tsxFilenames.add(filename)

    // Aussi chercher les references au module.section dans le contenu
    const routeRefs = content.match(/['"]([a-z][-a-z0-9]*\.[a-z][-a-z0-9]*)['"]/g)
    if (routeRefs) {
      for (const ref of routeRefs) {
        tsxFilenames.add(ref.replace(/['"]/g, ''))
      }
    }
  }

  const missing: string[] = []
  let okCount = 0

  for (const res of resources) {
    const navRoute = `${res.meta.module}.${res.meta.section}`
    const resourceLower = res.meta.resource.toLowerCase()

    const hasController = controllerNavRoutes.has(navRoute)

    // Chercher une page .tsx qui correspond au nom de la ressource
    const hasScreen = tsxFilenames.has(resourceLower) ||
      tsxFilenames.has(`${resourceLower}page`) ||
      tsxFilenames.has(`${resourceLower}list`) ||
      tsxFilenames.has(`${resourceLower}s`) ||
      tsxFilenames.has(navRoute)

    if (hasController && hasScreen) {
      okCount++
    } else {
      const parts: string[] = []
      if (!hasController) parts.push('controller missing')
      if (!hasScreen) parts.push('screen missing')
      missing.push(`${navRoute} (${res.meta.resource}): ${parts.join(', ')}`)
    }
  }

  if (missing.length > 0) {
    return {
      id: 'resources-have-screens',
      status: 'error',
      details: missing.join('; '),
    }
  }

  return { id: 'resources-have-screens', status: 'ok', count: okCount }
}

// ─── Check 5: rbac-consistent ───
// Verifie que toutes les permissions declarees dans permissions.json sont utilisees

function checkRbacConsistent(
  resources: ResourceSpec[],
  csFiles: string[],
  tsFiles: string[],
): CheckResult {
  // Collecter toutes les permissions declarees dans les specs
  const declaredPerms = new Map<string, string>()

  for (const res of resources) {
    if (!res.permissions) continue
    const permClass = res.permissions.class
    for (const perm of res.permissions.permissions) {
      const fullPerm = `${permClass}.${perm}`
      declaredPerms.set(fullPerm, `${res.meta.module}.${res.meta.section}`)
    }
  }

  if (declaredPerms.size === 0) {
    return { id: 'rbac-consistent', status: 'skipped', details: 'No permissions declared in specs' }
  }

  // Collecter les permissions effectivement utilisees dans le code
  const usedPerms = new Set<string>()

  // Dans les fichiers .cs
  for (const file of csFiles) {
    const content = readFile(file)
    const permRegex = /\[RequirePermission\(([^)]+)\)\]/g
    let match
    while ((match = permRegex.exec(content)) !== null) {
      usedPerms.add(match[1].trim())
    }
  }

  // Dans les fichiers .ts/.tsx
  for (const file of tsFiles) {
    const content = readFile(file)

    const guardRegex = /PermissionGuard[^>]*permission[=:]\s*["{]([^"}>]+)/g
    let match
    while ((match = guardRegex.exec(content)) !== null) {
      usedPerms.add(match[1].trim())
    }

    const hookRegex = /usePermission\(\s*["']?([^"')]+)["']?\s*\)/g
    while ((match = hookRegex.exec(content)) !== null) {
      usedPerms.add(match[1].trim())
    }
  }

  const unused: string[] = []
  let usedCount = 0

  for (const [perm, source] of declaredPerms) {
    if (usedPerms.has(perm)) {
      usedCount++
    } else {
      unused.push(`${perm} (from ${source})`)
    }
  }

  if (unused.length > 0) {
    return {
      id: 'rbac-consistent',
      status: 'warning',
      details: `Declared but unused permissions: ${unused.join('; ')}`,
    }
  }

  return { id: 'rbac-consistent', status: 'ok', count: usedCount }
}

// ─── Orchestration ───

export function execute(input: CrossValidateInput): CrossValidateReport {
  const { specs, src } = input

  // Charger les specs
  const resources = loadAllSpecs(specs)

  // Trouver les fichiers source
  const csFiles = findFilesByPattern(src, /Controller\.cs$/)
  const tsFiles = findFiles(src, '.ts')
  const tsxFiles = findFiles(src, '.tsx')
  const allTsFiles = [...tsFiles, ...tsxFiles]

  // Executer les 5 checks
  const checks: CheckResult[] = [
    checkRoutesHaveEndpoints(resources, csFiles, src),
    checkEndpointsAreConsumed(csFiles, allTsFiles, src),
    checkPermissionsAligned(csFiles, allTsFiles, src),
    checkResourcesHaveScreens(resources, csFiles, tsxFiles, src),
    checkRbacConsistent(resources, csFiles, allTsFiles),
  ]

  const errors = checks.filter(c => c.status === 'error').length
  const warnings = checks.filter(c => c.status === 'warning').length

  return {
    timestamp: new Date().toISOString(),
    checks,
    errors,
    warnings,
  }
}
