/**
 * cli:roslyn-validator — execute.ts
 * 10 regles SmartStack (SS001-SS010) par analyse regex des fichiers .cs
 * Ne fait PAS de validation d'input — c'est le role de validate.ts
 */
import { readFileSync, readdirSync, statSync } from 'node:fs'
import { join, basename, relative } from 'node:path'
import type { RoslynInput, CheckResult, RoslynReport } from './types'

// ─── Utilitaires ───

/** Parcours recursif pour trouver les fichiers .cs */
function findCsFiles(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()) {
      // Skip common non-source directories
      if (entry === 'bin' || entry === 'obj' || entry === 'node_modules' || entry === '.git') continue
      results.push(...findCsFiles(fullPath, pattern))
    } else if (entry.endsWith('.cs')) {
      if (!pattern || pattern.test(entry)) {
        results.push(fullPath)
      }
    }
  }
  return results
}

/** Lit un fichier et retourne ses lignes */
function readLines(filePath: string): string[] {
  return readFileSync(filePath, 'utf-8').split(/\r?\n/)
}

// ─── Regles SS001-SS010 ───

/** SS001: Herite ControllerBase + [ApiController] */
function checkSS001(files: string[], srcDir: string): CheckResult[] {
  const results: CheckResult[] = []
  if (files.length === 0) {
    results.push({ code: 'SS001', status: 'skipped', message: 'No controller files found' })
    return results
  }

  let okCount = 0
  for (const file of files) {
    const content = readFileSync(file, 'utf-8')
    const relPath = relative(srcDir, file)
    const hasApiController = /\[ApiController\]/.test(content)
    const hasControllerBase = /class\s+\w+Controller[^{]*:\s*[^{]*ControllerBase/.test(content)

    if (hasApiController && hasControllerBase) {
      okCount++
    } else {
      const missing: string[] = []
      if (!hasApiController) missing.push('[ApiController]')
      if (!hasControllerBase) missing.push('ControllerBase inheritance')
      results.push({
        code: 'SS001',
        status: 'error',
        file: relPath,
        message: `Missing ${missing.join(' and ')}`,
      })
    }
  }

  if (okCount > 0 && results.length === 0) {
    results.push({ code: 'SS001', status: 'ok', count: okCount })
  }
  return results
}

/** SS002: Injection unique IService (pas DbContext) */
function checkSS002(files: string[], srcDir: string): CheckResult[] {
  const results: CheckResult[] = []
  if (files.length === 0) {
    results.push({ code: 'SS002', status: 'skipped', message: 'No controller files found' })
    return results
  }

  let okCount = 0
  for (const file of files) {
    const content = readFileSync(file, 'utf-8')
    const lines = content.split(/\r?\n/)
    const relPath = relative(srcDir, file)

    // Check for forbidden DbContext injection
    const hasDbContext = /DbContext|ICoreDbContext/.test(content)
    if (hasDbContext) {
      // Find line number
      const lineNum = lines.findIndex(l => /DbContext|ICoreDbContext/.test(l))
      results.push({
        code: 'SS002',
        status: 'error',
        file: relPath,
        line: lineNum >= 0 ? lineNum + 1 : undefined,
        message: 'Controller injects DbContext directly — must use IService only',
      })
      continue
    }

    // Find constructor and check it has a single IService or ISender parameter
    const constructorMatch = content.match(/public\s+\w+Controller\s*\(([^)]*)\)/)
    if (constructorMatch) {
      const params = constructorMatch[1].trim()
      if (params.length > 0) {
        // Split parameters by comma, trimming each
        const paramList = params.split(',').map(p => p.trim()).filter(p => p.length > 0)
        // Check that all params are IService or ISender types
        const nonServiceParams = paramList.filter(p => {
          return !(/^I\w+Service\s/.test(p) || /^ISender\s/.test(p))
        })
        if (nonServiceParams.length > 0) {
          const lineNum = lines.findIndex(l => /public\s+\w+Controller\s*\(/.test(l))
          results.push({
            code: 'SS002',
            status: 'warning',
            file: relPath,
            line: lineNum >= 0 ? lineNum + 1 : undefined,
            message: `Constructor has non-service parameter(s): ${nonServiceParams.map(p => p.split(/\s/)[0]).join(', ')}`,
          })
          continue
        }
      }
    }

    okCount++
  }

  if (okCount > 0 && results.filter(r => r.status === 'error').length === 0) {
    results.unshift({ code: 'SS002', status: 'ok', count: okCount })
  }
  return results
}

/** SS003: [NavRoute] present et format module.section */
function checkSS003(files: string[], srcDir: string): CheckResult[] {
  const results: CheckResult[] = []
  if (files.length === 0) {
    results.push({ code: 'SS003', status: 'skipped', message: 'No controller files found' })
    return results
  }

  let okCount = 0
  for (const file of files) {
    const content = readFileSync(file, 'utf-8')
    const lines = content.split(/\r?\n/)
    const relPath = relative(srcDir, file)

    const navRouteMatch = content.match(/\[NavRoute\("([^"]*)"\)\]/)
    if (!navRouteMatch) {
      results.push({
        code: 'SS003',
        status: 'error',
        file: relPath,
        message: 'Missing [NavRoute] attribute',
      })
      continue
    }

    const routeValue = navRouteMatch[1]
    if (!/^\w+\.\w+/.test(routeValue)) {
      const lineNum = lines.findIndex(l => /\[NavRoute\(/.test(l))
      results.push({
        code: 'SS003',
        status: 'error',
        file: relPath,
        line: lineNum >= 0 ? lineNum + 1 : undefined,
        message: `[NavRoute] value "${routeValue}" does not match format module.section`,
      })
      continue
    }

    okCount++
  }

  if (okCount > 0 && results.filter(r => r.status === 'error').length === 0) {
    results.unshift({ code: 'SS003', status: 'ok', count: okCount })
  }
  return results
}

/** SS004: [RequirePermission] sur chaque endpoint public */
function checkSS004(files: string[], srcDir: string): CheckResult[] {
  const results: CheckResult[] = []
  if (files.length === 0) {
    results.push({ code: 'SS004', status: 'skipped', message: 'No controller files found' })
    return results
  }

  let okCount = 0
  let hasError = false
  for (const file of files) {
    const lines = readLines(file)
    const relPath = relative(srcDir, file)
    let fileOk = true

    for (let i = 0; i < lines.length; i++) {
      const line = lines[i]
      // Detect HTTP method attributes
      const httpMatch = line.match(/\[(Http(?:Get|Post|Put|Delete|Patch))/)
      if (!httpMatch) continue

      // Look for [RequirePermission] in the 5 lines before and 5 lines after
      const windowStart = Math.max(0, i - 5)
      const windowEnd = Math.min(lines.length - 1, i + 5)
      let hasPermission = false
      for (let j = windowStart; j <= windowEnd; j++) {
        if (/\[RequirePermission/.test(lines[j])) {
          hasPermission = true
          break
        }
      }

      if (!hasPermission) {
        results.push({
          code: 'SS004',
          status: 'error',
          file: relPath,
          line: i + 1,
          message: `Endpoint [${httpMatch[1]}] sans [RequirePermission]`,
        })
        fileOk = false
        hasError = true
      }
    }

    if (fileOk) okCount++
  }

  if (okCount > 0 && !hasError) {
    results.unshift({ code: 'SS004', status: 'ok', count: okCount })
  }
  return results
}

/** SS005: Retourne DTOs, jamais entites */
function checkSS005(files: string[], srcDir: string): CheckResult[] {
  const results: CheckResult[] = []
  if (files.length === 0) {
    results.push({ code: 'SS005', status: 'skipped', message: 'No controller files found' })
    return results
  }

  let okCount = 0
  let hasError = false
  for (const file of files) {
    const lines = readLines(file)
    const relPath = relative(srcDir, file)
    let fileOk = true

    for (let i = 0; i < lines.length; i++) {
      const line = lines[i]
      // Detect returning entities directly: Ok(entity), return entity
      if (/\bOk\(\s*entity\b/.test(line) || /\breturn\s+entity\b/.test(line)) {
        results.push({
          code: 'SS005',
          status: 'error',
          file: relPath,
          line: i + 1,
          message: 'Returns entity directly instead of DTO',
        })
        fileOk = false
        hasError = true
      }
    }

    if (fileOk) okCount++
  }

  if (okCount > 0 && !hasError) {
    results.unshift({ code: 'SS005', status: 'ok', count: okCount })
  }
  return results
}

/** SS006: CancellationToken dernier parametre des methodes async publiques */
function checkSS006(files: string[], srcDir: string): CheckResult[] {
  const results: CheckResult[] = []
  if (files.length === 0) {
    results.push({ code: 'SS006', status: 'skipped', message: 'No controller files found' })
    return results
  }

  let okCount = 0
  let hasError = false
  for (const file of files) {
    const content = readFileSync(file, 'utf-8')
    const lines = content.split(/\r?\n/)
    const relPath = relative(srcDir, file)
    let fileOk = true

    // Match public async methods with their full parameter list
    // We need to handle multi-line signatures, so join and search
    const joined = lines.join('\n')
    const methodRegex = /public\s+async\s+Task[^(]*\(([^)]*)\)/g
    let match
    while ((match = methodRegex.exec(joined)) !== null) {
      const paramsStr = match[1].trim()
      if (paramsStr.length === 0) continue

      const params = paramsStr.split(',').map(p => p.trim())
      const lastParam = params[params.length - 1]

      if (!/CancellationToken/.test(lastParam)) {
        // Find line number of this match
        const beforeMatch = joined.substring(0, match.index)
        const lineNum = beforeMatch.split('\n').length
        results.push({
          code: 'SS006',
          status: 'error',
          file: relPath,
          line: lineNum,
          message: 'Async public method missing CancellationToken as last parameter',
        })
        fileOk = false
        hasError = true
      }
    }

    if (fileOk) okCount++
  }

  if (okCount > 0 && !hasError) {
    results.unshift({ code: 'SS006', status: 'ok', count: okCount })
  }
  return results
}

/** SS007: NavRoute unique dans le projet */
function checkSS007(files: string[], srcDir: string): CheckResult[] {
  const results: CheckResult[] = []
  if (files.length === 0) {
    results.push({ code: 'SS007', status: 'skipped', message: 'No controller files found' })
    return results
  }

  // Collect all NavRoute values across all controllers
  const routeMap = new Map<string, string[]>()
  for (const file of files) {
    const content = readFileSync(file, 'utf-8')
    const relPath = relative(srcDir, file)
    const navRouteMatch = content.match(/\[NavRoute\("([^"]*)"\)\]/)
    if (navRouteMatch) {
      const routeValue = navRouteMatch[1]
      const existing = routeMap.get(routeValue) || []
      existing.push(relPath)
      routeMap.set(routeValue, existing)
    }
  }

  let hasDuplicate = false
  for (const [route, routeFiles] of routeMap.entries()) {
    if (routeFiles.length > 1) {
      for (const f of routeFiles) {
        results.push({
          code: 'SS007',
          status: 'error',
          file: f,
          message: `Duplicate NavRoute "${route}" — also in: ${routeFiles.filter(x => x !== f).join(', ')}`,
        })
      }
      hasDuplicate = true
    }
  }

  if (!hasDuplicate) {
    results.push({ code: 'SS007', status: 'ok', count: routeMap.size })
  }
  return results
}

/** SS008: Service sans DbContext */
function checkSS008(files: string[], srcDir: string): CheckResult[] {
  const results: CheckResult[] = []
  if (files.length === 0) {
    results.push({ code: 'SS008', status: 'skipped', message: 'No service files found' })
    return results
  }

  let okCount = 0
  for (const file of files) {
    const content = readFileSync(file, 'utf-8')
    const lines = content.split(/\r?\n/)
    const relPath = relative(srcDir, file)

    const dbContextMatch = /DbContext|ICoreDbContext/.test(content)
    if (dbContextMatch) {
      const lineNum = lines.findIndex(l => /DbContext|ICoreDbContext/.test(l))
      results.push({
        code: 'SS008',
        status: 'error',
        file: relPath,
        line: lineNum >= 0 ? lineNum + 1 : undefined,
        message: 'Service uses DbContext directly — must use IRepository',
      })
    } else {
      okCount++
    }
  }

  if (okCount > 0 && results.filter(r => r.status === 'error').length === 0) {
    results.unshift({ code: 'SS008', status: 'ok', count: okCount })
  }
  return results
}

/** SS009: Repository sans logique metier */
function checkSS009(files: string[], srcDir: string): CheckResult[] {
  const results: CheckResult[] = []
  if (files.length === 0) {
    results.push({ code: 'SS009', status: 'skipped', message: 'No repository files found' })
    return results
  }

  let okCount = 0
  let hasError = false
  for (const file of files) {
    const lines = readLines(file)
    const relPath = relative(srcDir, file)
    let fileOk = true

    for (let i = 0; i < lines.length; i++) {
      const line = lines[i]
      // Detect throw ...Exception, but allow NotFoundException and ArgumentNullException
      const throwMatch = line.match(/throw\s+new\s+(\w*Exception)/)
      if (throwMatch) {
        const exType = throwMatch[1]
        const allowed = ['NotFoundException', 'ArgumentNullException', 'KeyNotFoundException']
        if (!allowed.some(a => exType.includes(a))) {
          results.push({
            code: 'SS009',
            status: 'error',
            file: relPath,
            line: i + 1,
            message: `Repository throws ${exType} — business logic belongs in Service layer`,
          })
          fileOk = false
          hasError = true
        }
      }
    }

    if (fileOk) okCount++
  }

  if (okCount > 0 && !hasError) {
    results.unshift({ code: 'SS009', status: 'ok', count: okCount })
  }
  return results
}

/** SS010: Entity herite BaseEntity */
function checkSS010(files: string[], srcDir: string): CheckResult[] {
  const results: CheckResult[] = []
  if (files.length === 0) {
    results.push({ code: 'SS010', status: 'skipped', message: 'No entity files found in Domain' })
    return results
  }

  let okCount = 0
  for (const file of files) {
    const content = readFileSync(file, 'utf-8')
    const relPath = relative(srcDir, file)

    // Check for class declaration inheriting BaseEntity
    const hasClassDecl = /class\s+\w+/.test(content)
    if (!hasClassDecl) continue // not a class file, skip

    // Must check it is an entity class (has a class declaration) and inherits BaseEntity
    const inheritsBase = /class\s+\w+[^{]*:\s*[^{]*BaseEntity/.test(content)

    // Skip abstract base classes, interfaces, enums, configuration classes, etc.
    const isAbstract = /abstract\s+class/.test(content)
    const isConfig = /IEntityTypeConfiguration/.test(content)
    const isInterface = /^\s*public\s+interface\s/m.test(content)
    const isEnum = /^\s*public\s+enum\s/m.test(content)

    if (isAbstract || isConfig || isInterface || isEnum) continue

    if (inheritsBase) {
      okCount++
    } else {
      results.push({
        code: 'SS010',
        status: 'error',
        file: relPath,
        message: 'Entity class does not inherit BaseEntity',
      })
    }
  }

  if (okCount > 0 && results.filter(r => r.status === 'error').length === 0) {
    results.unshift({ code: 'SS010', status: 'ok', count: okCount })
  } else if (results.length === 0) {
    results.push({ code: 'SS010', status: 'skipped', message: 'No entity classes found in Domain' })
  }
  return results
}

// ─── Orchestration ───

export function execute(input: RoslynInput): RoslynReport {
  const { src, layer } = input

  // Collect files per pattern
  const controllerFiles = (layer === 'all' || layer === 'controller')
    ? findCsFiles(src, /Controller\.cs$/)
    : []

  const serviceFiles = (layer === 'all' || layer === 'service')
    ? findCsFiles(src, /Service\.cs$/)
    : []

  const repositoryFiles = (layer === 'all' || layer === 'repository')
    ? findCsFiles(src, /Repository\.cs$/)
    : []

  // For entities, look specifically in Domain directories
  const entityFiles = (layer === 'all' || layer === 'entity')
    ? findEntityFiles(src)
    : []

  const checks: CheckResult[] = []

  // Controller rules (SS001-SS007)
  if (layer === 'all' || layer === 'controller') {
    checks.push(...checkSS001(controllerFiles, src))
    checks.push(...checkSS002(controllerFiles, src))
    checks.push(...checkSS003(controllerFiles, src))
    checks.push(...checkSS004(controllerFiles, src))
    checks.push(...checkSS005(controllerFiles, src))
    checks.push(...checkSS006(controllerFiles, src))
    checks.push(...checkSS007(controllerFiles, src))
  }

  // Service rule (SS008)
  if (layer === 'all' || layer === 'service') {
    checks.push(...checkSS008(serviceFiles, src))
  }

  // Repository rule (SS009)
  if (layer === 'all' || layer === 'repository') {
    checks.push(...checkSS009(repositoryFiles, src))
  }

  // Entity rule (SS010)
  if (layer === 'all' || layer === 'entity') {
    checks.push(...checkSS010(entityFiles, src))
  }

  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,
  }
}

/** Find entity files in Domain directories */
function findEntityFiles(src: string): string[] {
  // Look for .cs files inside directories named "Domain", "Entities", or "Models"
  // that are likely entity classes
  const domainFiles: string[] = []

  function scanForDomainDirs(dir: string): void {
    let entries: string[]
    try {
      entries = readdirSync(dir)
    } catch {
      return
    }
    for (const entry of entries) {
      const fullPath = join(dir, entry)
      let stat
      try {
        stat = statSync(fullPath)
      } catch {
        continue
      }
      if (!stat.isDirectory()) continue
      if (entry === 'bin' || entry === 'obj' || entry === 'node_modules' || entry === '.git') continue

      const lowerEntry = entry.toLowerCase()
      // Match exact "Domain"/"Entities" or patterns like "Ba011.Domain"
      if (lowerEntry === 'domain' || lowerEntry === 'entities'
        || lowerEntry.endsWith('.domain') || lowerEntry.endsWith('.entities')) {
        // Collect all .cs files in this directory tree
        domainFiles.push(...findCsFiles(fullPath))
      } else {
        scanForDomainDirs(fullPath)
      }
    }
  }

  scanForDomainDirs(src)

  // If no Domain/Entities directories found, return empty
  // (don't scan entire project — too many false positives)
  return domainFiles
}
