import fs from "fs"
import path from "path"
import { Glob } from "@opencode-ai/core/util/glob"

type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }

interface ModuleFact {
  name: string
  root: string
  moduleType?: string
  mainElement?: string
  moduleJson?: string
  abilities: Array<{ name?: string; srcEntry?: string }>
}

interface CodeFact {
  path: string
  exportedSymbols: string[]
  components: string[]
  entryComponents: string[]
  loadContentTargets: string[]
  stateObjects: string[]
}

interface PriorityModule {
  module: string
  files: string[]
  chain?: string
}

function rel(filePath: string, root: string) {
  return path.relative(root, filePath).split(path.sep).join("/")
}

function readText(filePath: string) {
  return fs.readFileSync(filePath, "utf8")
}

function stripJson5Comments(text: string) {
  return text
    .replace(/\/\*[\s\S]*?\*\//g, "")
    .replace(/(^|[^:])\/\/.*$/gm, "$1")
}

function quoteUnquotedKeys(text: string) {
  return text.replace(/([{\[,]\s*)([A-Za-z_][\w-]*)(\s*:)/g, '$1"$2"$3')
}

function removeTrailingCommas(text: string) {
  return text.replace(/,\s*([}\]])/g, "$1")
}

function parseJson5Like(filePath: string): JsonValue {
  const text = removeTrailingCommas(quoteUnquotedKeys(stripJson5Comments(readText(filePath)))).replace(/'/g, '"')
  return JSON.parse(text) as JsonValue
}

function asObject(value: JsonValue | undefined) {
  if (!value || typeof value !== "object" || Array.isArray(value)) return undefined
  return value as Record<string, JsonValue>
}

function asArray(value: JsonValue | undefined) {
  return Array.isArray(value) ? value : []
}

function exists(filePath: string) {
  try {
    return fs.statSync(filePath).isFile()
  } catch {
    return false
  }
}

function isHarmonyProject(projectRoot: string) {
  return (
    exists(path.join(projectRoot, "AppScope", "app.json5")) ||
    (exists(path.join(projectRoot, "build-profile.json5")) &&
      (exists(path.join(projectRoot, "oh-package.json5")) || exists(path.join(projectRoot, "oh-package.json"))))
  )
}

function discoverModuleRoots(projectRoot: string) {
  const roots = new Map<string, string>()
  const buildProfile = path.join(projectRoot, "build-profile.json5")
  if (exists(buildProfile)) {
    const data = asObject(parseJson5Like(buildProfile))
    for (const item of asArray(data?.modules)) {
      const module = asObject(item)
      const name = module?.name
      const srcPath = module?.srcPath
      if (typeof name === "string" && typeof srcPath === "string") {
        roots.set(name, path.resolve(projectRoot, srcPath))
      }
    }
  }

  for (const moduleJson of Glob.scanSync("**/src/main/module.json5", {
    cwd: projectRoot,
    absolute: true,
    dot: true,
    symlink: true,
  })) {
    try {
      const data = asObject(parseJson5Like(moduleJson))
      const module = asObject(data?.module)
      const name = module?.name
      if (typeof name !== "string") continue
      const moduleRoot = path.resolve(moduleJson, "..", "..", "..")
      roots.set(name, moduleRoot)
    } catch {
      continue
    }
  }

  return roots
}

function scanModules(projectRoot: string) {
  const modules = new Map<string, ModuleFact>()
  for (const [name, moduleRoot] of discoverModuleRoots(projectRoot)) {
    const moduleJson = path.join(moduleRoot, "src", "main", "module.json5")
    const fact: ModuleFact = { name, root: rel(moduleRoot, projectRoot), abilities: [] }
    if (!exists(moduleJson)) {
      modules.set(name, fact)
      continue
    }

    const data = asObject(parseJson5Like(moduleJson))
    const module = asObject(data?.module)
    fact.moduleJson = rel(moduleJson, projectRoot)
    fact.moduleType = typeof module?.type === "string" ? module.type : undefined
    fact.mainElement = typeof module?.mainElement === "string" ? module.mainElement : undefined
    fact.abilities = asArray(module?.abilities)
      .map((item) => asObject(item))
      .filter((item): item is { name?: string; srcEntry?: string } => !!item)
      .map((item) => ({
        name: typeof item.name === "string" ? item.name : undefined,
        srcEntry: typeof item.srcEntry === "string" ? item.srcEntry : undefined,
      }))
    modules.set(name, fact)
  }
  return modules
}

function extractExportedSymbols(text: string) {
  const patterns = [
    /export\s+(?:default\s+)?class\s+(\w+)/g,
    /export\s+struct\s+(\w+)/g,
    /export\s+function\s+(\w+)/g,
    /export\s+enum\s+(\w+)/g,
    /export\s+interface\s+(\w+)/g,
  ]
  const symbols = new Set<string>()
  for (const pattern of patterns) {
    for (const match of text.matchAll(pattern)) {
      if (match[1]) symbols.add(match[1])
    }
  }
  return [...symbols]
}

function extractComponents(text: string) {
  const components = new Set<string>()
  const entryComponents = new Set<string>()
  const pattern = /(@Entry\s*)?@Component(?:\([^)]*\))?\s*(?:export\s+)?struct\s+(\w+)/gs
  for (const match of text.matchAll(pattern)) {
    if (!match[2]) continue
    components.add(match[2])
    if (match[1]) entryComponents.add(match[2])
  }
  return { components: [...components], entryComponents: [...entryComponents] }
}

function extractStateObjects(text: string) {
  const suffixes = "(?:ViewModel|VM|Store|Controller|Presenter|Model|State)"
  const patterns = [
    new RegExp(`\\bnew\\s+([A-Z]\\w*${suffixes})\\s*\\(`, "g"),
    new RegExp(`\\b([A-Z]\\w*${suffixes})\\.getInstance\\s*\\(`, "g"),
  ]
  const symbols = new Set<string>()
  for (const pattern of patterns) {
    for (const match of text.matchAll(pattern)) {
      if (match[1]) symbols.add(match[1])
    }
  }
  return [...symbols]
}

function scanEtsFiles(projectRoot: string) {
  const facts = new Map<string, CodeFact>()
  for (const filePath of Glob.scanSync("**/*.ets", {
    cwd: projectRoot,
    absolute: true,
    dot: true,
    symlink: true,
  })) {
    const normalized = filePath.replace(/\\/g, "/")
    if (normalized.includes("/ohosTest/") || normalized.includes("/src/test/") || normalized.includes("/.preview/")) {
      continue
    }

    const text = readText(filePath)
    const { components, entryComponents } = extractComponents(text)
    const fact: CodeFact = {
      path: rel(filePath, projectRoot),
      exportedSymbols: extractExportedSymbols(text),
      components,
      entryComponents,
      loadContentTargets: [...text.matchAll(/\.loadContent\s*\(\s*['"]([^'"]+)['"]/g)].map((match) => match[1]),
      stateObjects: extractStateObjects(text),
    }

    if (
      fact.exportedSymbols.length ||
      fact.components.length ||
      fact.entryComponents.length ||
      fact.loadContentTargets.length ||
      fact.stateObjects.length
    ) {
      facts.set(fact.path, fact)
    }
  }
  return facts
}

function resolveModuleSrcEntry(moduleJson: string, srcEntry: string) {
  const base = path.dirname(moduleJson)
  const candidate = path.resolve(base, srcEntry)
  if (exists(candidate)) return candidate
  if (srcEntry.startsWith("./")) {
    const trimmed = path.resolve(base, srcEntry.slice(2))
    if (exists(trimmed)) return trimmed
  }
  return candidate
}

function resolveLoadContent(moduleRoot: string, target: string) {
  const targetPath = target.endsWith(".ets") ? target.slice(0, -4) : target
  const candidates = [
    path.join(moduleRoot, "src", "main", "ets", `${targetPath}.ets`),
    path.join(moduleRoot, "src", "main", "ets", targetPath),
  ]
  return candidates.find((candidate) => exists(candidate))
}

function commonPathPrefixLen(left: string, right: string) {
  const leftParts = left.split("/")
  const rightParts = right.split("/")
  let count = 0
  for (let index = 0; index < Math.min(leftParts.length, rightParts.length); index++) {
    if (leftParts[index] !== rightParts[index]) break
    count++
  }
  return count
}

function findStateObjectFile(codeFacts: Map<string, CodeFact>, stateObject: string, sourcePath?: string) {
  const candidates = [...codeFacts.entries()]
    .filter(
      ([, fact]) =>
        fact.exportedSymbols.includes(stateObject) ||
        fact.components.includes(stateObject) ||
        fact.entryComponents.includes(stateObject),
    )
    .map(([filePath]) => filePath)
  if (!candidates.length) return undefined
  if (!sourcePath) return candidates[0]
  return candidates.reduce((best, candidate) =>
    commonPathPrefixLen(sourcePath, candidate) > commonPathPrefixLen(sourcePath, best) ? candidate : best,
  )
}

function uniqueKeepOrder(items: Array<string | undefined>) {
  const seen = new Set<string>()
  const result: string[] = []
  for (const item of items) {
    if (!item || seen.has(item)) continue
    seen.add(item)
    result.push(item)
  }
  return result
}

function projectPriorityConfigs(projectRoot: string) {
  return uniqueKeepOrder([
    exists(path.join(projectRoot, "build-profile.json5")) ? rel(path.join(projectRoot, "build-profile.json5"), projectRoot) : undefined,
    exists(path.join(projectRoot, "AppScope", "app.json5")) ? rel(path.join(projectRoot, "AppScope", "app.json5"), projectRoot) : undefined,
    exists(path.join(projectRoot, "oh-package.json5")) ? rel(path.join(projectRoot, "oh-package.json5"), projectRoot) : undefined,
  ])
}

function routeStartupEntryFlow(projectRoot: string, modules: Map<string, ModuleFact>, codeFacts: Map<string, CodeFact>) {
  const priorityModules: PriorityModule[] = []

  for (const module of modules.values()) {
    if (module.moduleType !== "entry" || !module.mainElement || !module.moduleJson) continue

    const priorityFiles: string[] = [module.moduleJson]
    let abilitySource: string | undefined
    let firstPage: string | undefined
    let firstPageSource: string | undefined

    for (const ability of module.abilities) {
      if (ability.name !== module.mainElement || !ability.srcEntry) continue
      const abilityPath = resolveModuleSrcEntry(path.join(projectRoot, module.moduleJson), ability.srcEntry)
      abilitySource = rel(abilityPath, projectRoot)
      priorityFiles.push(abilitySource)
      break
    }

    if (abilitySource) {
      const abilityFact = codeFacts.get(abilitySource)
      if (abilityFact?.loadContentTargets[0]) {
        firstPage = abilityFact.loadContentTargets[0]
        const firstPagePath = resolveLoadContent(path.join(projectRoot, module.root), firstPage)
        if (firstPagePath) {
          firstPageSource = rel(firstPagePath, projectRoot)
          priorityFiles.push(firstPageSource)
        }
      }
    }

    if (firstPageSource) {
      const firstPageFact = codeFacts.get(firstPageSource)
      for (const stateObject of firstPageFact?.stateObjects ?? []) {
        const stateObjectFile = findStateObjectFile(codeFacts, stateObject, firstPageSource)
        if (stateObjectFile) priorityFiles.push(stateObjectFile)
      }
    }

    const chain = [
      module.moduleJson,
      abilitySource,
      firstPage ? `loadContent(${firstPage})` : undefined,
      firstPageSource,
    ]
      .filter(Boolean)
      .join(" -> ")

    priorityModules.push({
      module: module.name,
      files: uniqueKeepOrder(priorityFiles),
      chain: chain || undefined,
    })
  }

  return priorityModules
}

function formatExploreContext(projectRoot: string, priorityModules: PriorityModule[]) {
  if (!priorityModules.length) return undefined

  const lines = [
    "Task routing:",
    "- The section below lists project-specific priority files and startup chains from static configuration analysis.",
    "- Treat those files as the highest-priority cold-start context for startup, entry, launch-page, routing, and module-structure questions.",
    "- Start by reading the listed priority files in order before broad `glob`, `grep`, or directory traversal.",
    "- Use the startup chain to understand cross-file relationships such as `module.json5 -> EntryAbility -> loadContent(page) -> page source`.",
    "- If the task is unrelated to the listed routes, or the priority files are not enough, continue with normal search tools.",
    "",
    "# Task Routing",
    "",
    "This section is generated from static HarmonyOS project analysis. Use it to reduce cold-start blind search.",
    "",
    `Base path: ${projectRoot}`,
    "",
    "Before broad directory traversal or keyword guessing, read the priority files below when the task involves app startup, entry configuration, launch pages, or module structure.",
    "",
  ]

  const projectConfigs = projectPriorityConfigs(projectRoot)
  if (projectConfigs.length) {
    lines.push("## Project configuration", "Priority files:")
    for (const file of projectConfigs) lines.push(`- ${file}`)
    lines.push("")
  }

  for (const item of priorityModules) {
    lines.push(`## startup_entry_flow (${item.module})`)
    if (item.chain) lines.push(`Startup chain: ${item.chain}`)
    lines.push("Priority files:")
    for (const file of item.files) lines.push(`- ${file}`)
    lines.push("")
  }

  lines.push(
    "Search guidance:",
    "- Prefer `read` on the listed priority files before `glob` or `grep` when they match the task.",
    "- If the priority files are insufficient, continue with normal search tools.",
  )

  return lines.join("\n")
}

export function buildExploreContext(projectRoot: string) {
  const resolved = path.resolve(projectRoot)
  if (!isHarmonyProject(resolved)) return undefined

  const modules = scanModules(resolved)
  const codeFacts = scanEtsFiles(resolved)
  const priorityModules = routeStartupEntryFlow(resolved, modules, codeFacts)
  return formatExploreContext(resolved, priorityModules)
}