package com.correx.apps.server.memory

import com.correx.core.events.events.RepoMapEntry
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.extension
import kotlin.io.path.getLastModifiedTime
import kotlin.io.path.isDirectory
import kotlin.io.path.isRegularFile
import kotlin.io.path.name
import kotlin.io.path.readText
import kotlin.io.path.relativeTo

/**
 * Walks a repo and produces a ranked file/symbol index. Reads the filesystem (a
 * nondeterministic environment observation) — the caller records the result as a
 * [com.correx.core.events.events.RepoMapComputedEvent] so replay reads recorded facts and
 * never re-scans (invariant #9). Paths + top-level symbol names only, never file bodies.
 *
 * Scoring is recency-based: the most-recently-modified source file scores 1.0, the oldest
 * ~0.0, linearly between. Recency is a cheap centrality proxy — files under active work rank
 * highest, which is what a fresh session most wants to see.
 */
class RepoMapIndexer(
    private val maxDepth: Int = 4,
    private val ignoreGlobs: List<String> = emptyList(),
) {
    fun index(repoRoot: Path): List<RepoMapEntry> {
        if (!repoRoot.isDirectory()) return emptyList()
        val ignore = ignoreGlobs.toSet()

        val files = Files.walk(repoRoot, maxDepth).use { stream ->
            stream.filter { it.isRegularFile() }
                .filter { it.extension in SYMBOL_PATTERNS.keys }
                .filter { path -> path.relativeTo(repoRoot).none { it.name in ignore } }
                .toList()
        }
        if (files.isEmpty()) return emptyList()

        val mtimes = files.associateWith { runCatching { it.getLastModifiedTime().toMillis() }.getOrDefault(0L) }
        val min = mtimes.values.min()
        val max = mtimes.values.max()
        val span = (max - min).coerceAtLeast(1L)

        return files
            .map { path ->
                RepoMapEntry(
                    path = path.relativeTo(repoRoot).toString(),
                    score = (mtimes.getValue(path) - min).toDouble() / span,
                    symbols = extractSymbols(path),
                )
            }
            .sortedByDescending { it.score }
    }

    private fun extractSymbols(path: Path): List<String> {
        val regex = SYMBOL_PATTERNS[path.extension] ?: return emptyList()
        val text = runCatching { path.readText() }.getOrElse { return emptyList() }
        return regex.findAll(text)
            .mapNotNull { it.groups["name"]?.value }
            .distinct()
            .take(MAX_SYMBOLS_PER_FILE)
            .toList()
    }

    private companion object {
        const val MAX_SYMBOLS_PER_FILE = 40

        // Top-level declarations only — best-effort per language. Bodies/locals are never matched.
        val SYMBOL_PATTERNS: Map<String, Regex> = mapOf(
            "kt" to Regex("""(?m)^\s*(?:public |internal |open |abstract |sealed |data )*(?:class|interface|object|fun)\s+(?<name>[A-Za-z_][A-Za-z0-9_]*)"""),
            "java" to Regex("""(?m)^\s*(?:public |private |protected |final |abstract |static )*(?:class|interface|enum|record)\s+(?<name>[A-Za-z_][A-Za-z0-9_]*)"""),
            "py" to Regex("""(?m)^(?:def|class)\s+(?<name>[A-Za-z_][A-Za-z0-9_]*)"""),
            "go" to Regex("""(?m)^\s*(?:func\s+(?:\([^)]*\)\s*)?|type\s+)(?<name>[A-Za-z_][A-Za-z0-9_]*)"""),
            "js" to Regex("""(?m)^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function|class)\s+(?<name>[A-Za-z_$][A-Za-z0-9_$]*)"""),
            "ts" to Regex("""(?m)^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function|class|interface|type|enum)\s+(?<name>[A-Za-z_$][A-Za-z0-9_$]*)"""),
        )
    }
}
