import { createHash } from 'node:crypto'
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import { parse as parseYaml } from 'yaml'
import { type ScaffoldRegistry, type ScaffoldRegistryEntry, ScaffoldRegistrySchema } from './schema'

const MODULE_DIR = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1'))

export {
  ExternalSyncCategorySchema,
  ExternalSyncDeletePolicySchema,
  ExternalSyncOwnerSchema,
  ExternalSyncStrategySchema,
  ScaffoldEntryKindSchema,
  ScaffoldRegistrySchema
} from './schema'
export type {
  ExternalSyncCategory,
  ExternalSyncDeletePolicy,
  ExternalSyncOwner,
  ExternalSyncStrategy,
  ScaffoldEntryKind,
  ScaffoldRef,
  ScaffoldRegistry,
  ScaffoldRegistryEntry
} from './schema'

// ---------------------------------------------------------------------------
// Paths (resolved relative to the monorepo root, not this file's location)
// ---------------------------------------------------------------------------

/**
 * Resolve a path relative to the monorepo root.
 * Works whether this module is running from packages/core/src or from dist/.
 */
function monorepoRoot(): string {
  // Walk up from __dirname until we find the .claude directory (monorepo marker)
  const { dirname } = path
  let dir = MODULE_DIR
  for (let i = 0; i < 8; i++) {
    try {
      readFileSync(path.join(dir, '.claude', 'settings.json'))
      return dir
    } catch {
      dir = dirname(dir)
    }
  }
  throw new Error(
    'scaffold-registry: could not locate monorepo root (no .claude/settings.json found in ancestor directories)'
  )
}

const YAML_FILENAME = '.claude/registries/scaffold-registry.yml'
const JSON_FILENAME = '.claude/registries/scaffold-registry.compiled.json'

// ---------------------------------------------------------------------------
// Load + validate
// ---------------------------------------------------------------------------

/**
 * Load and Zod-validate the scaffold registry from `.claude/registries/scaffold-registry.yml`.
 *
 * Throws if:
 * - The YAML file is missing or unreadable
 * - The YAML fails Zod validation
 * - The compiled JSON is present but its sha256 hash differs from the YAML
 *   (drift detection — regenerate with `compileScaffoldRegistry()` to fix)
 *
 * @param rootDir - Optional monorepo root override. Defaults to the auto-detected
 *   monorepo root. Pass a fixture directory to test against a local snapshot.
 */
export function loadScaffoldRegistry(rootDir?: string): ScaffoldRegistry {
  const root = rootDir ?? monorepoRoot()
  const yamlPath = path.join(root, YAML_FILENAME)

  let raw: string
  try {
    raw = readFileSync(yamlPath, 'utf-8')
  } catch (err) {
    throw new Error(`scaffold-registry: could not read ${YAML_FILENAME} — ${String(err)}`)
  }

  const parsed = parseYaml(raw) as unknown
  const result = ScaffoldRegistrySchema.safeParse(parsed)

  if (!result.success) {
    const issues = result.error.issues.map((i) => `  [${i.path.join('.')}] ${i.message}`).join('\n')
    throw new Error(`scaffold-registry: YAML validation failed:\n${issues}`)
  }

  const registry = result.data

  // Drift check: if compiled JSON exists, verify the full normalized content matches.
  const jsonPath = path.join(root, JSON_FILENAME)
  try {
    const compiledRaw = readFileSync(jsonPath, 'utf-8')
    const compiledParsed = JSON.parse(compiledRaw) as unknown
    const compiledResult = ScaffoldRegistrySchema.safeParse(compiledParsed)
    if (!compiledResult.success) {
      const issues = compiledResult.error.issues.map((i) => `  [${i.path.join('.')}] ${i.message}`).join('\n')
      throw new Error(`scaffold-registry: compiled JSON validation failed:\n${issues}`)
    }

    const yamlHash = stableRegistryHash(registry)
    const compiledHash = stableRegistryHash(compiledResult.data)
    if (compiledHash !== yamlHash) {
      throw new Error(
        `scaffold-registry: compiled JSON is out of sync with YAML ` +
          `(YAML hash ${yamlHash.slice(0, 12)}, JSON hash ${compiledHash.slice(0, 12)}). ` +
          `Run compileScaffoldRegistry() to regenerate.`
      )
    }
  } catch (err) {
    // If the file doesn't exist, skip drift check silently (first-run scenario)
    if ((err as { code?: string }).code !== 'ENOENT') {
      throw err
    }
  }

  return registry
}

// ---------------------------------------------------------------------------
// Compile
// ---------------------------------------------------------------------------

/**
 * Load, validate, and write the pre-compiled JSON lookup file.
 * Run this whenever `.claude/registries/scaffold-registry.yml` changes.
 *
 * Called by the `pnpm scaffold:compile-registry` script.
 *
 * @param rootDir - Optional monorepo root override. Defaults to the auto-detected
 *   monorepo root. Pass a fixture directory to test against a local snapshot.
 */
export function compileScaffoldRegistry(rootDir?: string): ScaffoldRegistry {
  const root = rootDir ?? monorepoRoot()
  const registry = loadScaffoldRegistryNoSyncCheck(root)

  const missing = findMissingDependentPaths(registry, root)
  if (missing.length > 0) {
    const formatted = missing.map((m) => `  [${m.entryId}] ${m.path}`).join('\n')
    throw new Error(
      `scaffold-registry: ${missing.length} dependent path(s) do not exist on disk:\n${formatted}\n` +
        `Fix the typo, create the file, or convert the path to a glob/symbolic target.`
    )
  }

  const emptySources = findEmptySourcePatterns(registry, root)
  if (emptySources.length > 0) {
    const formatted = emptySources.map((source) => `  [${source.entryId}] ${source.pattern}`).join('\n')
    throw new Error(
      `scaffold-registry: ${emptySources.length} source pattern(s) match no files or directories:\n${formatted}\n` +
        `Fix the stale source glob, create the scaffold surface, or add explicit registry support for intentional empty patterns.`
    )
  }

  const jsonPath = path.join(root, JSON_FILENAME)
  writeFileSync(jsonPath, JSON.stringify(registry, null, 2) + '\n', 'utf-8')

  return registry
}

/**
 * Return dependent paths declared in the registry that don't exist on disk.
 * Skips symbolic targets (`docs:`, `autogen-target:`) and glob patterns
 * (those containing `*`, `?`, or `[`), which can't be resolved to a single file.
 *
 * Exported so external scripts (e.g. CI gates) can run the same check.
 */
export function findMissingDependentPaths(
  registry: ScaffoldRegistry,
  monorepoRootDir: string
): Array<{ entryId: string; path: string }> {
  const missing: Array<{ entryId: string; path: string }> = []
  for (const entry of registry.entries) {
    // sync-preservation dependents describe paths inside derived external
    // projects (not files that physically exist in this monorepo), so skip them.
    if (entry.kind === 'sync-preservation') continue
    for (const dependent of entry.dependents) {
      if (isSymbolicTarget(dependent.path) || isGlobPattern(dependent.path) || dependent.path === '(self)') {
        continue
      }
      const absolute = path.join(monorepoRootDir, dependent.path)
      if (!existsSync(absolute)) {
        missing.push({ entryId: entry.id, path: dependent.path })
      }
    }
  }
  return missing
}

/**
 * Return source patterns that do not currently match any file or directory.
 * Symbolic sources are skipped because they intentionally do not resolve to
 * monorepo paths.
 */
export function findEmptySourcePatterns(
  registry: ScaffoldRegistry,
  monorepoRootDir: string
): Array<{ entryId: string; pattern: string }> {
  const empty: Array<{ entryId: string; pattern: string }> = []

  for (const entry of registry.entries) {
    for (const sourcePattern of entry.sources) {
      if (isSymbolicTarget(sourcePattern)) continue
      if (!sourcePatternMatchesAnyPath(sourcePattern, monorepoRootDir)) {
        empty.push({ entryId: entry.id, pattern: sourcePattern })
      }
    }
  }

  return empty
}

// ---------------------------------------------------------------------------
// Lookup helpers (used by hooks for fast path matching)
// ---------------------------------------------------------------------------

/**
 * Load from the pre-compiled JSON only (fastest path; used by PostToolUse hooks).
 * Falls back to YAML if the JSON is missing.
 */
export function loadScaffoldRegistryFast(): ScaffoldRegistry {
  const root = monorepoRoot()
  const jsonPath = path.join(root, JSON_FILENAME)

  try {
    const raw = readFileSync(jsonPath, 'utf-8')
    const parsed = JSON.parse(raw) as unknown
    const result = ScaffoldRegistrySchema.safeParse(parsed)
    if (result.success) return result.data
  } catch {
    // fall through to YAML
  }

  return loadScaffoldRegistryNoSyncCheck(root)
}

/**
 * Return all entries whose `sources` contain at least one pattern that matches
 * the given file path AND whose `excludes` contain no pattern that matches.
 * Pattern matching is a simple substring/glob-prefix check suitable for hook
 * use; Step 3 will upgrade to full micromatch when the hook is implemented.
 */
export function findMatchingEntries(registry: ScaffoldRegistry, filePath: string): ScaffoldRegistryEntry[] {
  return registry.entries.filter((entry) => {
    const sourceMatch = entry.sources.some((pattern) => scaffoldPathMatchesPattern(filePath, pattern))
    if (!sourceMatch) return false
    const excluded = (entry.excludes ?? []).some((pattern) => scaffoldPathMatchesPattern(filePath, pattern))
    return !excluded
  })
}

// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------

function loadScaffoldRegistryNoSyncCheck(root: string): ScaffoldRegistry {
  const yamlPath = path.join(root, YAML_FILENAME)
  let raw: string
  try {
    raw = readFileSync(yamlPath, 'utf-8')
  } catch (err) {
    throw new Error(`scaffold-registry: could not read ${YAML_FILENAME} — ${String(err)}`)
  }
  const parsed = parseYaml(raw) as unknown
  const result = ScaffoldRegistrySchema.safeParse(parsed)
  if (!result.success) {
    const issues = result.error.issues.map((i) => `  [${i.path.join('.')}] ${i.message}`).join('\n')
    throw new Error(`scaffold-registry: YAML validation failed:\n${issues}`)
  }
  return result.data
}

export function normalizeScaffoldPath(p: string): string {
  return p.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '')
}

export function scaffoldPathMatchesPattern(filePath: string, pattern: string): boolean {
  const normalizedFile = normalizeScaffoldPath(filePath)
  const normalizedPattern = normalizeScaffoldPath(pattern)

  if (!normalizedFile || !normalizedPattern) return false
  if (normalizedFile === normalizedPattern) return true

  if (!isGlobPattern(normalizedPattern)) {
    return normalizedFile.startsWith(normalizedPattern + '/')
  }

  return globToRegExp(normalizedPattern).test(normalizedFile)
}

function isSymbolicTarget(p: string): boolean {
  return p.startsWith('docs:') || p.startsWith('autogen-target:')
}

function isGlobPattern(p: string): boolean {
  return /[*?[]/.test(p)
}

function stableRegistryHash(registry: ScaffoldRegistry): string {
  return createHash('sha256').update(JSON.stringify(registry)).digest('hex')
}

// Published-boundary note: scripts/lib/glob-matcher.mjs carries a behaviorally
// identical copy of globToRegExp. The duplication is INTENTIONAL and must NOT be
// collapsed: @repo/core is a published npm package that cannot import from scripts/,
// and scripts/ cannot import across the published boundary. If you change the
// algorithm here, mirror it in scripts/lib/glob-matcher.mjs.
function globToRegExp(pattern: string): RegExp {
  const segments = pattern.split('/')
  let regex = '^'

  for (let index = 0; index < segments.length; index++) {
    const segment = segments[index]
    const isLast = index === segments.length - 1

    if (segment === '**') {
      regex += isLast ? '(?:.*)?' : '(?:[^/]+/)*'
      continue
    }

    regex += segmentToRegExpSource(segment)
    if (!isLast) regex += '/'
  }

  regex += '$'
  return new RegExp(regex)
}

function segmentToRegExpSource(segment: string): string {
  let source = ''
  for (let index = 0; index < segment.length; index++) {
    const char = segment[index]
    if (char === '*') {
      source += '[^/]*'
    } else if (char === '?') {
      source += '[^/]'
    } else {
      source += escapeRegExp(char)
    }
  }
  return source
}

function escapeRegExp(value: string): string {
  return value.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')
}

function sourcePatternMatchesAnyPath(sourcePattern: string, monorepoRootDir: string): boolean {
  const normalizedPattern = normalizeScaffoldPath(sourcePattern)

  if (!isGlobPattern(normalizedPattern)) {
    return existsSync(path.join(monorepoRootDir, normalizedPattern))
  }

  const baseDir = findGlobBaseDirectory(normalizedPattern)
  const absoluteBase = path.join(monorepoRootDir, baseDir)
  if (!existsSync(absoluteBase)) return false

  return walkUntilMatch(absoluteBase, monorepoRootDir, normalizedPattern)
}

function findGlobBaseDirectory(pattern: string): string {
  const segments = pattern.split('/')
  const baseSegments: string[] = []
  for (const segment of segments) {
    if (isGlobPattern(segment)) break
    baseSegments.push(segment)
  }
  return baseSegments.join('/')
}

function walkUntilMatch(currentPath: string, monorepoRootDir: string, pattern: string): boolean {
  const rel = normalizeScaffoldPath(path.relative(monorepoRootDir, currentPath))
  if (rel && scaffoldPathMatchesPattern(rel, pattern)) return true

  let entries
  try {
    entries = readdirSync(currentPath, { withFileTypes: true })
  } catch {
    return false
  }

  for (const entry of entries) {
    const absolutePath = path.join(currentPath, entry.name)
    const relativePath = normalizeScaffoldPath(path.relative(monorepoRootDir, absolutePath))

    if (scaffoldPathMatchesPattern(relativePath, pattern)) return true
    if (entry.isDirectory()) {
      try {
        if (statSync(absolutePath).isDirectory() && walkUntilMatch(absolutePath, monorepoRootDir, pattern)) {
          return true
        }
      } catch {
        continue
      }
    }
  }

  return false
}
