/**
 * cli:validate-page — execute.ts
 *
 * Eight blocking rules. Each rule is a standalone function consuming the
 * file content (and optional context: tsconfig paths, locale roots) and
 * returning Violation[]. The aggregator below runs all rules and assembles
 * the final report.
 *
 * Determinism: zero LLM calls, zero network. Pure regex + filesystem reads.
 */
import { existsSync, readFileSync } from 'node:fs'
import { isAbsolute, join, relative, resolve, sep, dirname } from 'node:path'
import type { ValidatePageInput, ValidatePageReport, Violation } from './types.js'

const LOCALES = ['fr', 'en', 'it', 'de'] as const
const PERMISSION_KEY_REGEX = /^[a-z][a-z0-9-]*\.[a-z][a-z0-9-]*\.[a-z][a-z0-9-]*$/

interface LocationContext {
  module: string | null
  entity: string | null
  view: string | null
}

interface RuleContext {
  /** Absolute path to the page file. */
  pageFile: string
  /** Web root (parent of src/). */
  projectPath: string
  /** Page content (utf-8). */
  source: string
  /** Lines of the source (1-based access via `lines[i-1]`). */
  lines: string[]
  loc: LocationContext
  /** tsconfig#paths map ('@' alias → absolute filesystem prefix). */
  aliases: Record<string, string>
}

// ─── Helpers ───────────────────────────────────────────────────────────────

function parseLocation(absPath: string, projectPath: string): LocationContext {
  const rel = relative(projectPath, absPath).split(sep).join('/')
  const m = rel.match(
    /^src\/pages\/[^/]+\/([^/]+)\/[^/]+\/([A-Z][A-Za-z0-9]+?)(List|Detail|Form|Dashboard|Create|Edit|New)Page\.tsx$/,
  )
  if (!m) return { module: null, entity: null, view: null }
  return { module: m[1], entity: m[2], view: m[3].toLowerCase() }
}

function readTsconfigAliases(projectPath: string): Record<string, string> {
  const candidates = ['tsconfig.json', 'tsconfig.app.json', 'tsconfig.base.json']
  for (const file of candidates) {
    const abs = join(projectPath, file)
    if (!existsSync(abs)) continue
    try {
      const raw = readFileSync(abs, 'utf-8')
      // Strip JSON-with-comments
      const cleaned = raw
        .replace(/\/\*[\s\S]*?\*\//g, '')
        .replace(/^\s*\/\/.*$/gm, '')
        .replace(/,\s*([}\]])/g, '$1')
      const parsed = JSON.parse(cleaned)
      const paths = parsed?.compilerOptions?.paths
      if (paths && typeof paths === 'object') {
        const out: Record<string, string> = {}
        for (const [alias, targets] of Object.entries(paths)) {
          if (!Array.isArray(targets) || targets.length === 0) continue
          // alias is like '@/*'; target is like 'src/*'.
          const aliasPrefix = alias.replace(/\/\*$/, '/')
          const targetPrefix = String(targets[0]).replace(/\/\*$/, '/')
          out[aliasPrefix] = join(projectPath, targetPrefix)
        }
        if (Object.keys(out).length > 0) return out
      }
    } catch {
      // ignore — fall through to default
    }
  }
  // Default SmartStack alias.
  return { '@/': join(projectPath, 'src/') }
}

function resolveImport(spec: string, fromFile: string, ctx: RuleContext): string | null {
  // External package — skip.
  if (!spec.startsWith('.') && !spec.startsWith('@/') && !spec.startsWith('/')) return null

  let absBase: string
  if (spec.startsWith('.')) {
    absBase = resolve(dirname(fromFile), spec)
  } else if (spec.startsWith('@/')) {
    const aliasMatch = Object.entries(ctx.aliases).find(([prefix]) => spec.startsWith(prefix))
    if (!aliasMatch) return null
    absBase = join(aliasMatch[1], spec.slice(aliasMatch[0].length))
  } else {
    absBase = spec
  }

  const tries = [
    absBase,
    `${absBase}.ts`,
    `${absBase}.tsx`,
    `${absBase}.js`,
    `${absBase}.jsx`,
    `${absBase}.json`,
    join(absBase, 'index.ts'),
    join(absBase, 'index.tsx'),
    join(absBase, 'index.js'),
  ]
  for (const candidate of tries) {
    if (existsSync(candidate)) return candidate
  }
  return null
}

function findLineNumber(source: string, needle: string, startFrom = 0): number | undefined {
  const idx = source.indexOf(needle, startFrom)
  if (idx < 0) return undefined
  let line = 1
  for (let i = 0; i < idx; i++) {
    if (source[i] === '\n') line++
  }
  return line
}

// ─── Rules ─────────────────────────────────────────────────────────────────

function ruleImportsResolve(ctx: RuleContext): Violation[] {
  const out: Violation[] = []
  const importRe = /^\s*import\s+(?:[^'"]+\sfrom\s+)?['"]([^'"]+)['"];?\s*$/gm
  let m: RegExpExecArray | null
  while ((m = importRe.exec(ctx.source)) !== null) {
    const spec = m[1]
    // External packages handled in resolveImport (returns null for non-relative non-aliased).
    if (!spec.startsWith('.') && !spec.startsWith('@/')) continue
    const resolved = resolveImport(spec, ctx.pageFile, ctx)
    if (resolved === null) {
      out.push({
        rule: 'imports-resolve',
        severity: 'err',
        line: findLineNumber(ctx.source, m[0].trimStart()),
        message: `Import '${spec}' does not resolve to a file on disk (page: ${relative(ctx.projectPath, ctx.pageFile)}).`,
        suggestedFix: spec.startsWith('@/components/ui/PageTemplate')
          ? 'Re-run scaffold-layout — it writes src/components/ui/PageTemplate.tsx.'
          : spec.startsWith('@/features/')
          ? 'Re-run scaffold-api-client for the corresponding entity to emit hooks/services/types.'
          : undefined,
      })
    }
  }
  return out
}

function rulePageTemplateWrapping(ctx: RuleContext): Violation[] {
  const out: Violation[] = []
  const importPattern = /import\s*\{\s*PageTemplate\s*\}\s*from\s*['"]@\/components\/ui\/PageTemplate['"]/
  if (!importPattern.test(ctx.source)) {
    out.push({
      rule: 'pagetemplate-wrapping',
      severity: 'err',
      message: `Page does not import PageTemplate from '@/components/ui/PageTemplate'. The page must use the canonical wrapper.`,
      suggestedFix: `Add: import { PageTemplate } from '@/components/ui/PageTemplate'`,
    })
  }
  if (!/<PageTemplate[\s>]/.test(ctx.source)) {
    out.push({
      rule: 'pagetemplate-wrapping',
      severity: 'err',
      message: 'Page does not render <PageTemplate>. Wrap the page content in <PageTemplate title=…>.',
      suggestedFix: 'Wrap the JSX root in <PageTemplate title={t("…")}>…</PageTemplate>.',
    })
  }
  return out
}

function ruleUseParamsNullCheck(ctx: RuleContext): Violation[] {
  const out: Violation[] = []
  const useParamsRe = /useParams<[^>]+>\s*\(\s*\)/g
  let m: RegExpExecArray | null
  while ((m = useParamsRe.exec(ctx.source)) !== null) {
    const lineIdx = (ctx.source.slice(0, m.index).match(/\n/g) ?? []).length
    const window = ctx.lines.slice(lineIdx, lineIdx + 9).join('\n')
    const guarded = /if\s*\(\s*!?\s*id\s*\)/.test(window) || /if\s*\(\s*id\s*===\s*undefined\s*\)/.test(window)
    if (!guarded) {
      out.push({
        rule: 'useparams-null-check',
        severity: 'err',
        line: lineIdx + 1,
        message: `useParams<…>() is not null-checked within 8 lines. Accessing 'id' as non-null risks a runtime crash on the create route.`,
        suggestedFix: `Add: if (!id) return <Navigate to=".." replace />; before any data hook.`,
      })
    }
  }
  return out
}

function rulePermissionGuardOnMutations(ctx: RuleContext): Violation[] {
  const out: Violation[] = []
  // Any element triggering a mutating action.
  const mutationRe = /(mutateAsync|mutation\.mutate|useDelete|useUpdate|useCreate)/g
  let m: RegExpExecArray | null
  const seenLines = new Set<number>()
  while ((m = mutationRe.exec(ctx.source)) !== null) {
    const lineIdx = (ctx.source.slice(0, m.index).match(/\n/g) ?? []).length + 1
    if (seenLines.has(lineIdx)) continue
    seenLines.add(lineIdx)
    // Scan upwards for an enclosing PermissionGuard within 30 lines.
    const start = Math.max(0, lineIdx - 30)
    const window = ctx.lines.slice(start, lineIdx).join('\n')
    if (!/<PermissionGuard\s+[^>]*permission\s*=/.test(window)) {
      out.push({
        rule: 'permissionguard-on-mutations',
        severity: 'err',
        line: lineIdx,
        message: `Mutation reference '${m[1]}' is not wrapped in <PermissionGuard permission="…"> within 30 lines.`,
        suggestedFix: `Wrap the action button (or surrounding JSX block) in <PermissionGuard permission="{module}.{section}.{create|update|delete}">.`,
      })
    }
  }
  return out
}

function ruleNoLocalUsePermissions(ctx: RuleContext): Violation[] {
  const out: Violation[] = []
  const re = /(?:^|\n)\s*(function\s+usePermissions\s*\(|const\s+usePermissions\s*=)/g
  let m: RegExpExecArray | null
  while ((m = re.exec(ctx.source)) !== null) {
    const lineIdx = (ctx.source.slice(0, m.index).match(/\n/g) ?? []).length + 1
    out.push({
      rule: 'no-local-usepermissions',
      severity: 'err',
      line: lineIdx,
      message:
        'Page declares a local usePermissions() stub. This bypasses the SmartStack permission system — every check defaults to true.',
      suggestedFix:
        'Remove the local stub. Use <PermissionGuard permission="{module}.{section}.{action}"> from @atlashub/smartstack instead.',
    })
  }
  return out
}

interface ParsedI18nKey {
  namespace: string | null
  key: string
  line: number
}

function ruleI18nKeysResolve(ctx: RuleContext): Violation[] {
  if (!ctx.loc.module || !ctx.loc.entity) return []
  const out: Violation[] = []

  // Find every `useTranslation('NS')` to map default namespace.
  const nsCalls = [...ctx.source.matchAll(/useTranslation\(\s*['"]([^'"]+)['"]\s*\)/g)]
  const defaultNs = nsCalls.length > 0 ? nsCalls[0][1] : ctx.loc.entity.toLowerCase()

  // Find every `t('key')` (or with namespace prefix `t('ns:key')`).
  const calls: ParsedI18nKey[] = []
  const tRe = /\bt\(\s*['"]([^'"]+)['"]/g
  let m: RegExpExecArray | null
  while ((m = tRe.exec(ctx.source)) !== null) {
    const raw = m[1]
    const lineIdx = (ctx.source.slice(0, m.index).match(/\n/g) ?? []).length + 1
    let namespace: string | null
    let key: string
    if (raw.includes(':')) {
      const [ns, ...rest] = raw.split(':')
      namespace = ns
      key = rest.join(':')
    } else {
      namespace = defaultNs
      key = raw
    }
    calls.push({ namespace, key, line: lineIdx })
  }

  if (calls.length === 0) return []

  // For each (namespace, locale) pair, load the JSON once and walk dotted keys.
  const loadedLocales = new Map<string, Record<string, unknown> | null>()
  function loadLocale(locale: string, namespace: string): Record<string, unknown> | null {
    const cacheKey = `${locale}::${namespace}`
    if (loadedLocales.has(cacheKey)) return loadedLocales.get(cacheKey) ?? null
    const abs = join(ctx.projectPath, 'src', 'i18n', locale, ctx.loc.module ?? '', `${namespace}.json`)
    if (!existsSync(abs)) {
      loadedLocales.set(cacheKey, null)
      return null
    }
    try {
      const raw = JSON.parse(readFileSync(abs, 'utf-8')) as Record<string, unknown>
      loadedLocales.set(cacheKey, raw)
      return raw
    } catch {
      loadedLocales.set(cacheKey, null)
      return null
    }
  }
  function resolveKey(tree: Record<string, unknown>, dottedKey: string): boolean {
    let node: unknown = tree
    for (const segment of dottedKey.split('.')) {
      if (node === null || typeof node !== 'object') return false
      node = (node as Record<string, unknown>)[segment]
      if (node === undefined) return false
    }
    return typeof node === 'string'
  }

  for (const call of calls) {
    if (!call.namespace) continue
    for (const locale of LOCALES) {
      const tree = loadLocale(locale, call.namespace)
      if (tree === null) {
        out.push({
          rule: 'i18n-keys-resolve',
          severity: 'err',
          line: call.line,
          message: `i18n locale file missing: src/i18n/${locale}/${ctx.loc.module}/${call.namespace}.json (key '${call.key}' will render as the raw string).`,
          suggestedFix: `Re-run scaffold-component for entity '${ctx.loc.entity}' to emit the missing locale file.`,
        })
        continue
      }
      if (!resolveKey(tree, call.key)) {
        out.push({
          rule: 'i18n-keys-resolve',
          severity: 'err',
          line: call.line,
          message: `i18n key '${call.key}' missing in src/i18n/${locale}/${ctx.loc.module}/${call.namespace}.json.`,
          suggestedFix: `Add a translation for '${call.key}' or remove the t() call.`,
        })
      }
    }
  }

  return out
}

function ruleHookImportsExist(ctx: RuleContext): Violation[] {
  const out: Violation[] = []
  const importRe = /import\s*\{\s*([^}]+)\s*\}\s*from\s*['"](@\/features\/[^'"]+)['"]/g
  let m: RegExpExecArray | null
  while ((m = importRe.exec(ctx.source)) !== null) {
    const names = m[1]
      .split(',')
      .map((n) => n.replace(/\s+as\s+\w+/, '').trim())
      .filter((n) => n.length > 0 && !n.startsWith('type '))
    if (names.length === 0) continue
    const spec = m[2]
    const resolved = resolveImport(spec, ctx.pageFile, ctx)
    if (!resolved) continue // already flagged by ruleImportsResolve
    let exportSrc: string
    try {
      exportSrc = readFileSync(resolved, 'utf-8')
    } catch {
      continue
    }
    for (const name of names) {
      const exportPattern = new RegExp(
        `export\\s+(?:async\\s+)?(?:function|const|class|interface|type)\\s+${name}\\b|export\\s*\\{[^}]*\\b${name}\\b`,
      )
      if (!exportPattern.test(exportSrc)) {
        out.push({
          rule: 'hook-imports-exist',
          severity: 'err',
          line: findLineNumber(ctx.source, m[0]),
          message: `Imported symbol '${name}' is not exported by ${spec} (resolves to ${relative(ctx.projectPath, resolved)}).`,
          suggestedFix: `Re-run scaffold-api-client for the parent entity. If the symbol is a dashboard hook, ensure hasDashboard:true on the entity in slices.frontend.`,
        })
      }
    }
  }
  return out
}

function rulePermissionKeysNoAppCode(ctx: RuleContext): Violation[] {
  const out: Violation[] = []
  const re = /permission\s*=\s*['"]([^'"]+)['"]/g
  let m: RegExpExecArray | null
  while ((m = re.exec(ctx.source)) !== null) {
    const value = m[1]
    if (!PERMISSION_KEY_REGEX.test(value)) {
      const lineIdx = (ctx.source.slice(0, m.index).match(/\n/g) ?? []).length + 1
      const segments = value.split('.').length
      out.push({
        rule: 'permission-keys-no-appcode',
        severity: 'err',
        line: lineIdx,
        message:
          segments === 4
            ? `permission='${value}' carries an appCode prefix (4 segments). Use {module}.{section}.{action} only.`
            : `permission='${value}' does not match {module}.{section}.{action} (lowercase, exactly 3 dot-segments).`,
        suggestedFix:
          segments === 4
            ? `Drop the leading appCode: '${value.split('.').slice(1).join('.')}'.`
            : `Rewrite as '<module>.<section>.<action>' with lowercase module/section and one of read/create/update/delete as action.`,
      })
    }
  }
  return out
}

/**
 * Rule 9 — No hardcoded Tailwind color classes or hex values in className.
 * Generated pages must use SmartStack CSS variables (`var(--color-accent-*)`,
 * `var(--bg-*)`, etc.) — never Tailwind's built-in color palette (`bg-red-500`)
 * or inline hex (`bg-[#ff0000]`).
 */
function ruleCssHardcodedColors(ctx: RuleContext): Violation[] {
  const out: Violation[] = []
  // Match className="..." and className={`...`} attribute values.
  const classNameRe = /className\s*=\s*(?:"([^"]+)"|{`([^`]+)`})/g
  // Tailwind named color classes: bg-red-500, text-blue-600, border-green-300, etc.
  const namedColorRe = /\b(?:bg|text|border|ring|outline|shadow|from|via|to|divide|placeholder|accent|caret|fill|stroke)-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d{2,3}\b/g
  // Hardcoded hex in arbitrary values: bg-[#ff0000], text-[#333]
  const hexArbitraryRe = /\b(?:bg|text|border|ring|fill|stroke)-\[#[0-9a-fA-F]{3,8}\]/g
  // dark: prefix (should use CSS vars that auto-adapt)
  const darkPrefixRe = /\bdark:/g

  let m: RegExpExecArray | null
  while ((m = classNameRe.exec(ctx.source)) !== null) {
    const value = m[1] ?? m[2] ?? ''
    const lineIdx = (ctx.source.slice(0, m.index).match(/\n/g) ?? []).length + 1

    let colorMatch: RegExpExecArray | null
    namedColorRe.lastIndex = 0
    while ((colorMatch = namedColorRe.exec(value)) !== null) {
      out.push({
        rule: 'css-hardcoded-colors',
        severity: 'err',
        line: lineIdx,
        message: `Hardcoded Tailwind color '${colorMatch[0]}' — use a CSS variable token (e.g. bg-[var(--bg-card)], text-[var(--text-primary)]).`,
        suggestedFix: `Replace '${colorMatch[0]}' with the matching SmartStack CSS variable.`,
      })
    }

    hexArbitraryRe.lastIndex = 0
    while ((colorMatch = hexArbitraryRe.exec(value)) !== null) {
      out.push({
        rule: 'css-hardcoded-colors',
        severity: 'err',
        line: lineIdx,
        message: `Hardcoded hex color '${colorMatch[0]}' — use a CSS variable token instead.`,
        suggestedFix: `Replace '${colorMatch[0]}' with the matching SmartStack CSS variable.`,
      })
    }

    darkPrefixRe.lastIndex = 0
    if (darkPrefixRe.test(value)) {
      out.push({
        rule: 'css-hardcoded-colors',
        severity: 'err',
        line: lineIdx,
        message: `'dark:' prefix detected — SmartStack CSS variables handle dark mode automatically via .dark scope.`,
        suggestedFix: `Remove 'dark:' prefixed classes and use CSS variable tokens that auto-adapt.`,
      })
    }
  }

  return out
}

/**
 * Rule 10 — Breadcrumbs present on every page that renders <PageTemplate>.
 * The breadcrumbs prop ensures the user can navigate back up the hierarchy.
 */
function ruleBreadcrumbPresent(ctx: RuleContext): Violation[] {
  const out: Violation[] = []
  if (/<PageTemplate[\s>]/.test(ctx.source) && !/breadcrumbs\s*=\s*\{/.test(ctx.source)) {
    out.push({
      rule: 'breadcrumb-present',
      severity: 'err',
      message: `<PageTemplate> is rendered without a breadcrumbs prop. Add breadcrumbs={[…]} for navigational context.`,
      suggestedFix: `Add: breadcrumbs={[{ label: t('breadcrumb.section'), href: '..' }, { label: t('…') }]}`,
    })
  }
  return out
}

/**
 * Rule 11 — Tab navigation persists in URL via ?tab= search param.
 * Pages with tab-like patterns must use useSearchParams (or useTabNavigation)
 * to keep tab state in the URL for shareability and back-button support.
 */
function ruleTabUrlPersistence(ctx: RuleContext): Violation[] {
  const out: Violation[] = []
  // Detect tab-like patterns: activeTab state + conditional rendering
  const hasTabState = /activeTab|currentTab|selectedTab/.test(ctx.source)
  const usesUrlSync = /useSearchParams|useTabNavigation/.test(ctx.source)
  if (hasTabState && !usesUrlSync) {
    const line = findLineNumber(ctx.source, 'activeTab') ?? findLineNumber(ctx.source, 'currentTab') ?? findLineNumber(ctx.source, 'selectedTab')
    out.push({
      rule: 'tab-url-persistence',
      severity: 'err',
      line,
      message: `Tab state is managed in local React state only — tab selection is lost on page refresh or when sharing the URL.`,
      suggestedFix: `Use useSearchParams() or useTabNavigation() to persist the active tab in the URL (?tab=key).`,
    })
  }
  return out
}

/**
 * Rule 12 — Every tab label rendered via t('…') has a matching i18n key.
 * When a page uses tabs, the tab labels must have translated keys.
 */
function ruleTabI18nKeys(ctx: RuleContext): Violation[] {
  if (!ctx.loc.module || !ctx.loc.entity) return []
  const out: Violation[] = []
  // Find tab button patterns with t() labels
  const tabPatterns = /switchTab\(['"]([^'"]+)['"]\)[\s\S]{0,200}?\{(t\(['"]([^'"]+)['"]\))/g
  let m: RegExpExecArray | null
  while ((m = tabPatterns.exec(ctx.source)) !== null) {
    const tKey = m[3]
    if (!tKey) continue
    // The key resolution is handled by ruleI18nKeysResolve — this rule just
    // checks that tab buttons use t() instead of hardcoded strings.
    // Hardcoded tab label detection:
    const lineIdx = (ctx.source.slice(0, m.index).match(/\n/g) ?? []).length + 1
    if (!m[2].startsWith('t(')) {
      out.push({
        rule: 'tab-i18n-keys',
        severity: 'err',
        line: lineIdx,
        message: `Tab button label is not translated — use t('detail.tabs.${m[1]}') instead of a hardcoded string.`,
        suggestedFix: `Replace the label with t('detail.tabs.${m[1]}').`,
      })
    }
  }
  return out
}

function ruleCliGeneratedMarker(ctx: RuleContext): Violation[] {
  if (ctx.lines[0]?.startsWith('// @generated-by scaffold-component')) return []
  return [
    {
      rule: 'cli-generated-marker',
      severity: 'err' as const,
      message:
        'Page file is missing the `// @generated-by scaffold-component` header. ' +
        'This means it was hand-written instead of being produced by the deterministic CLI.',
      suggestedFix:
        'Delete the file and re-run scaffold-component via Bash — do NOT write .tsx files by hand in Phase 3a.',
    },
  ]
}

// ─── Entry point ───────────────────────────────────────────────────────────

export function execute(input: ValidatePageInput): ValidatePageReport {
  const projectPath = resolve(input.projectPath)
  const pageFile = isAbsolute(input.pageFile)
    ? resolve(input.pageFile)
    : resolve(join(projectPath, input.pageFile))

  if (!existsSync(pageFile)) {
    return {
      pageFile,
      module: null,
      entity: null,
      view: null,
      violations: [
        {
          rule: 'imports-resolve',
          severity: 'err',
          message: `pageFile does not exist on disk: ${pageFile}`,
          suggestedFix:
            'Run scaffold-component with the corresponding {module, entity, section, views:[…]} spec to create the file.',
        },
      ],
    }
  }

  const source = readFileSync(pageFile, 'utf-8')
  const lines = source.split(/\r?\n/)
  const loc = parseLocation(pageFile, projectPath)
  const aliases = readTsconfigAliases(projectPath)
  const ctx: RuleContext = { pageFile, projectPath, source, lines, loc, aliases }

  const violations: Violation[] = [
    ...ruleCliGeneratedMarker(ctx),
    ...ruleImportsResolve(ctx),
    ...rulePageTemplateWrapping(ctx),
    ...ruleUseParamsNullCheck(ctx),
    ...rulePermissionGuardOnMutations(ctx),
    ...ruleNoLocalUsePermissions(ctx),
    ...ruleI18nKeysResolve(ctx),
    ...ruleHookImportsExist(ctx),
    ...rulePermissionKeysNoAppCode(ctx),
    ...ruleCssHardcodedColors(ctx),
    ...ruleBreadcrumbPresent(ctx),
    ...ruleTabUrlPersistence(ctx),
    ...ruleTabI18nKeys(ctx),
  ]

  return {
    pageFile,
    module: loc.module,
    entity: loc.entity,
    view: loc.view,
    violations,
  }
}
