#!/usr/bin/env node
// AZ-485 / F4 (STC-ARCH-01) + AZ-486 / F7 (STC-ARCH-02).
//
// STC-ARCH-01 (mode=arch-imports, default) — no cross-component deep imports.
//   Every component under src/<component>/ exposes its Public API via a barrel
//   (src/<component>/index.ts). Cross-component imports MUST go through the
//   barrel; reaching into another component's internal files is a layering
//   violation.
//
// STC-ARCH-02 (mode=api-literals) — no hardcoded `/api/<service>/...` literals
//   in production source. The single source of truth for those URLs is
//   src/api/endpoints.ts (AZ-486 / F7). Any production callsite of api.* or
//   createSSE() that needs an API path must use an `endpoints.*` builder.
//
// Single source of truth — scripts/run-tests.sh delegates here, and the
// architecture unit test (tests/architecture_imports.test.ts) calls this
// script with synthetic fixtures to verify each gate fails on a regression
// and passes on the migrated codebase. Mirrors the scripts/check-banned-deps.mjs
// pattern (AZ-482).
//
// Usage:
//   node scripts/check-arch-imports.mjs [--mode=arch-imports|api-literals] [--root=<repo-root>]
//
// Exit code 0 on PASS; non-zero on FAIL with the hit list on stderr.

import { readFileSync, statSync, readdirSync } from 'node:fs'
import { join, dirname, resolve, relative } from 'node:path'
import { fileURLToPath } from 'node:url'

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)

const MODES = new Set(['arch-imports', 'api-literals'])

function parseArgs(argv) {
  const out = { root: resolve(__dirname, '..'), mode: 'arch-imports' }
  for (const a of argv.slice(2)) {
    if (a.startsWith('--root=')) out.root = resolve(a.slice('--root='.length))
    else if (a.startsWith('--mode=')) out.mode = a.slice('--mode='.length)
    else if (a === '-h' || a === '--help') {
      process.stderr.write(
        'Usage: check-arch-imports.mjs [--mode=arch-imports|api-literals] [--root=<repo-root>]\n',
      )
      process.exit(0)
    }
  }
  if (!MODES.has(out.mode)) {
    process.stderr.write(`unknown --mode=${out.mode}; expected one of: ${[...MODES].join(', ')}\n`)
    process.exit(2)
  }
  return out
}

const IGNORED_DIRS = new Set([
  'node_modules', 'dist', 'build', 'test-output', 'test-results',
  'coverage', '.git', '.cache', 'playwright-report', 'blob-report',
])
const SOURCE_EXT = new Set(['.ts', '.tsx'])

// ----------------------------------------------------------------------------
// STC-ARCH-01 — cross-component deep imports (mode=arch-imports)
// ----------------------------------------------------------------------------

// Cross-component deep-import pattern: `from '<up>/<src>?/<component>/<File>'`
//   - one or more `..` segments
//   - optional `src/` prefix (used by tests/ and e2e/ imports)
//   - a known component directory
//   - a path segment starting with a letter (i.e. NOT the barrel `index`)
//
// Allowed by construction:
//   - barrel:           from '../api'          (no further /<File>)
//   - intra-component:  from './sse'           (starts with ./, not ../)
const COMPONENT_DIRS = 'api|auth|class-colors|components|features/[a-z-]+|hooks|i18n'
const DEEP_IMPORT_RE = new RegExp(
  String.raw`from\s+['"](?:\.\./)+(?:src/)?(?:${COMPONENT_DIRS})/[A-Za-z]`,
)

// STC-ARCH-01 has no exemptions today. F3 (the classColors carry-over) was
// closed by AZ-511 — the file moved to its own component (`src/class-colors/`)
// with a proper barrel, and consumers now import via that barrel.
const ARCH_IMPORTS_EXEMPT_RE = null

const ARCH_IMPORTS_SCAN_ROOTS = ['src', 'tests', 'e2e']

// ----------------------------------------------------------------------------
// STC-ARCH-02 — hardcoded /api/<service>/ literals (mode=api-literals)
// ----------------------------------------------------------------------------

// Hardcoded API path pattern: a quote/backtick immediately followed by
// `/api/<lowercase-hyphen-service>/`. Catches single-quoted ('...'),
// double-quoted ("..."), and template-literal (`...`) styles equally — quote
// style is not a meaningful difference for "no hardcoded URLs in production".
//
// Examples that MATCH:
//   '/api/admin/users'
//   "/api/admin/auth/refresh"
//   `/api/annotations/dataset?${params}`
//   `/api/flights/${id}/waypoints`
//
// Examples that DO NOT match (intentional):
//   /api/admin (bare prefix, no trailing /)         — kept out so getApiBase()
//                                                     prefixes the base path
//                                                     unobstructed.
//   /api/users (no service segment, hypothetical)   — pattern requires the
//                                                     SECOND segment.
const API_LITERAL_RE = /[`'"]\/api\/[a-z][a-z-]*\//

// STC-ARCH-02 exemptions:
//   - the contract owner itself (src/api/endpoints.ts)
//   - test files (*.test.ts, *.test.tsx) — tests legitimately assert URL
//     strings, and the contract is precisely "the test file IS the contract"
const API_LITERAL_EXEMPT_FILES_RE = /(?:^|\/)(?:api\/endpoints\.ts|.+\.test\.tsx?)$/

const API_LITERALS_SCAN_ROOTS = ['src']

// ----------------------------------------------------------------------------
// Walk helpers
// ----------------------------------------------------------------------------

function* walkSourceFiles(rootDir) {
  let entries
  try {
    entries = readdirSync(rootDir, { withFileTypes: true })
  } catch {
    return
  }
  for (const entry of entries) {
    const full = join(rootDir, entry.name)
    if (entry.isDirectory()) {
      if (IGNORED_DIRS.has(entry.name)) continue
      yield* walkSourceFiles(full)
    } else if (entry.isFile()) {
      const dot = entry.name.lastIndexOf('.')
      if (dot < 0) continue
      const ext = entry.name.slice(dot)
      if (!SOURCE_EXT.has(ext)) continue
      yield full
    }
  }
}

function readLines(file) {
  try {
    return readFileSync(file, 'utf8').split('\n')
  } catch {
    return null
  }
}

// ----------------------------------------------------------------------------
// Scanners — one per mode. Both share the walker and line-comment skip.
// ----------------------------------------------------------------------------

function scanArchImports(file, root) {
  const hits = []
  const lines = readLines(file)
  if (lines === null) return hits
  const rel = relative(root, file).replaceAll('\\', '/')
  for (let i = 0; i < lines.length; i++) {
    const line = lines[i]
    if (/^\s*\/\//.test(line)) continue
    if (!DEEP_IMPORT_RE.test(line)) continue
    if (ARCH_IMPORTS_EXEMPT_RE && ARCH_IMPORTS_EXEMPT_RE.test(line)) continue
    hits.push(`${rel}:${i + 1}: ${line.trim().slice(0, 200)}`)
  }
  return hits
}

function scanApiLiterals(file, root) {
  const hits = []
  const rel = relative(root, file).replaceAll('\\', '/')
  if (API_LITERAL_EXEMPT_FILES_RE.test(rel)) return hits
  const lines = readLines(file)
  if (lines === null) return hits
  for (let i = 0; i < lines.length; i++) {
    const line = lines[i]
    if (/^\s*\/\//.test(line)) continue
    if (!API_LITERAL_RE.test(line)) continue
    hits.push(`${rel}:${i + 1}: ${line.trim().slice(0, 200)}`)
  }
  return hits
}

// ----------------------------------------------------------------------------
// Main
// ----------------------------------------------------------------------------

function main() {
  const { root, mode } = parseArgs(process.argv)
  const config = mode === 'api-literals'
    ? {
        roots: API_LITERALS_SCAN_ROOTS,
        scan: scanApiLiterals,
        failHeader:
          'STC-ARCH-02 — hardcoded /api/<service>/ literals detected in ' +
          'production source (must go through endpoints.* builders, see ' +
          'src/api/endpoints.ts):\n',
      }
    : {
        roots: ARCH_IMPORTS_SCAN_ROOTS,
        scan: scanArchImports,
        failHeader:
          'STC-ARCH-01 — cross-component deep imports detected ' +
          '(must go through component barrel, see module-layout.md):\n',
      }

  const hits = []
  for (const sub of config.roots) {
    const full = join(root, sub)
    try { statSync(full) } catch { continue }
    for (const file of walkSourceFiles(full)) {
      hits.push(...config.scan(file, root))
    }
  }
  if (hits.length) {
    process.stderr.write(config.failHeader)
    for (const h of hits) process.stderr.write(`  ${h}\n`)
    process.exit(1)
  }
  process.exit(0)
}

main()
