package com.correx.core.sourcedesc

/**
 * Format version of the descriptor derivation. Bump on any change to what [describe] extracts so a
 * recorded/rendered descriptor's provenance is explicit and a future extractor change is a visible
 * event, not a silent drift (the CAS post-image hash stays authoritative regardless).
 */
const val SOURCE_DESCRIPTOR_FORMAT: String = "sd/v1"

/**
 * Untrusted, non-prose navigation metadata derived from a source file's bytes.
 *
 * Descriptors are derived from agent-writable files and rendered into successor-stage context, so
 * they MUST NOT carry natural language: no comments, docstrings, string literals, or source
 * excerpts — any of which would be a prompt-injection channel from one stage into the next. Only
 * structural facts survive: module/package identity, bounded top-level symbol names, and bounded
 * import specifiers. The authority is always the file's CAS post-image hash; this is disposable
 * navigation metadata rendered as quoted workspace *data*, never an instruction-bearing directive.
 */
data class SourceDescriptor(
    val path: String,
    val extension: String,
    val module: String?,
    val symbols: List<String>,
    val imports: List<String>,
    val format: String = SOURCE_DESCRIPTOR_FORMAT,
) {
    /** One-line render as quoted workspace data (caller supplies any surrounding "role" framing). */
    fun render(): String = buildList {
        module?.let { add("module=$it") }
        if (symbols.isNotEmpty()) add("symbols=${symbols.joinToString(",")}")
        if (imports.isNotEmpty()) add("imports=${imports.joinToString(",")}")
    }.joinToString("; ").ifEmpty { extension }
}

private const val MAX_SYMBOLS = 40
private const val MAX_IMPORTS = 8

// Top-level declarations only — best-effort per language, bodies/locals never matched. Ported from
// apps/server's RepoMapIndexer, with .tsx/.jsx added (React components are the common case the
// original .ts/.js-only map missed) and the deliberate omission of any comment/prose capture.
private val SYMBOL_PATTERNS: Map<String, Regex> = mapOf(
    "kt" to Regex(
        """(?m)^\s*(?:public |internal |open |abstract |sealed |data )*""" +
            """(?:class|interface|object|fun)\s+([A-Za-z_][A-Za-z0-9_]*)""",
    ),
    "java" to Regex(
        """(?m)^\s*(?:public |private |protected |final |abstract |static )*""" +
            """(?:class|interface|enum|record)\s+([A-Za-z_][A-Za-z0-9_]*)""",
    ),
    "py" to Regex("""(?m)^(?:def|class)\s+([A-Za-z_][A-Za-z0-9_]*)"""),
    "go" to Regex("""(?m)^\s*(?:func\s+(?:\([^)]*\)\s*)?|type\s+)([A-Za-z_][A-Za-z0-9_]*)"""),
    "js" to Regex(
        """(?m)^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function|class)\s+""" +
            """([A-Za-z_$][A-Za-z0-9_$]*)""",
    ),
    "ts" to Regex(
        """(?m)^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function|class|interface|type|enum)\s+""" +
            """([A-Za-z_$][A-Za-z0-9_$]*)""",
    ),
    "gd" to Regex(
        """(?m)^(?:static\s+)?(?:func|class_name|class|signal|enum)\s+([A-Za-z_][A-Za-z0-9_]*)""",
    ),
    "cs" to Regex(
        """(?m)^\s*(?:public |private |protected |internal |static |sealed |abstract |""" +
            """partial )*(?:class|interface|struct|enum|record)\s+([A-Za-z_][A-Za-z0-9_]*)""",
    ),
).let { base ->
    // .tsx / .jsx reuse the ts / js declaration grammar.
    base + mapOf("tsx" to base.getValue("ts"), "jsx" to base.getValue("js"))
}

private val PACKAGE_OR_MODULE = Regex("""(?m)^\s*(?:package|module|namespace)\s+([A-Za-z0-9_.$/-]+)""")
private val IMPORT = Regex("""(?m)^\s*(?:import|using)\s+(?:.*?\bfrom\s+)?['"]?([A-Za-z0-9_.@$/*-]+)['"]?""")

/**
 * Derive a [SourceDescriptor] from a file path and its raw bytes. Pure: no filesystem access, no
 * clock, no external state — identical (path, content) always yields an identical descriptor, which
 * is what lets the kernel (replaying over CAS bytes) and apps/server (over live bytes) stay in
 * lockstep. Bytes are decoded as UTF-8 (lossy); unknown extensions yield an empty structural
 * descriptor rather than guessing.
 */
fun describe(path: String, content: ByteArray): SourceDescriptor {
    val ext = path.substringAfterLast('.', "").lowercase()
    val text = content.toString(Charsets.UTF_8)
    val module = PACKAGE_OR_MODULE.find(text)?.groupValues?.get(1)
    val symbols = SYMBOL_PATTERNS[ext]
        ?.findAll(text)
        ?.map { it.groupValues[1] }
        ?.distinct()
        ?.take(MAX_SYMBOLS)
        ?.toList()
        .orEmpty()
    val imports = IMPORT.findAll(text)
        .map { it.groupValues[1] }
        .distinct()
        .take(MAX_IMPORTS)
        .toList()
    return SourceDescriptor(path = path, extension = ext, module = module, symbols = symbols, imports = imports)
}
