package com.correx.core.validation.scene

import com.correx.core.validation.model.ValidationIssue
import com.correx.core.validation.model.ValidationReport
import com.correx.core.validation.model.ValidationSection
import com.correx.core.validation.model.ValidationSeverity

/**
 * Standalone structural validator for Godot `.tscn` / `.tres` text (an INI-like format).
 *
 * Pure logic: it takes the file content as a [String] and returns a [ValidationReport]. It does
 * NOT touch the filesystem and is intentionally not wired into any orchestration/pipeline.
 *
 * It checks three structural properties:
 *  1. Section headers are well-formed and of a known type.
 *  2. `ExtResource(...)` / `SubResource(...)` references resolve to a declared `id`.
 *  3. Node `parent="..."` paths resolve to an already-declared node.
 */
class SceneValidator {

    private companion object {
        const val CODE_MALFORMED_HEADER = "scene.malformed_header"
        const val CODE_UNKNOWN_SECTION = "scene.unknown_section"
        const val CODE_UNRESOLVED_RESOURCE = "scene.unresolved_resource"
        const val CODE_BAD_PARENT = "scene.bad_parent"

        val KNOWN_SECTIONS = setOf(
            "gd_scene",
            "gd_resource",
            "ext_resource",
            "sub_resource",
            "node",
            "resource",
            "connection",
        )

        val EXT_RESOURCE_REF = Regex("""ExtResource\(\s*"?([^")\s]+)"?\s*\)""")
        val SUB_RESOURCE_REF = Regex("""SubResource\(\s*"?([^")\s]+)"?\s*\)""")
    }

    fun validate(content: String): ValidationReport {
        val issues = mutableListOf<ValidationIssue>()
        val lines = content.split("\n").map { it.trim() }

        // Collect declared ids up front so resource-reference resolution is independent of the
        // order references appear relative to their declarations.
        val headers = lines.filter { it.startsWith("[") }.mapNotNull { parseHeader(it) }
        val extResourceIds = headers
            .filter { it.type == "ext_resource" }
            .mapNotNull { it.attributes["id"] }
            .toSet()
        val subResourceIds = headers
            .filter { it.type == "sub_resource" }
            .mapNotNull { it.attributes["id"] }
            .toSet()

        val knownNodePaths = mutableSetOf<String>()
        var rootDeclared = false

        lines.forEachIndexed { index, line ->
            val lineNumber = index + 1
            if (line.startsWith("[")) {
                val header = parseHeader(line)
                if (header == null) {
                    issues += issue(
                        CODE_MALFORMED_HEADER,
                        "Malformed section header at line $lineNumber: '$line'",
                        ValidationSeverity.ERROR,
                    )
                    return@forEachIndexed
                }
                if (header.type !in KNOWN_SECTIONS) {
                    issues += issue(
                        CODE_UNKNOWN_SECTION,
                        "Unknown section type '${header.type}' at line $lineNumber",
                        ValidationSeverity.WARNING,
                    )
                }
                if (header.type == "node") {
                    handleNode(header, lineNumber, rootDeclared, knownNodePaths, issues)
                    rootDeclared = true
                }
            } else {
                checkResourceReferences(line, lineNumber, extResourceIds, subResourceIds, issues)
            }
        }

        return ValidationReport(
            sections = listOf(ValidationSection(name = "scene", issues = issues)),
        )
    }

    private fun checkResourceReferences(
        line: String,
        lineNumber: Int,
        extResourceIds: Set<String>,
        subResourceIds: Set<String>,
        issues: MutableList<ValidationIssue>,
    ) {
        EXT_RESOURCE_REF.findAll(line).forEach { match ->
            val id = match.groupValues[1]
            if (id !in extResourceIds) {
                issues += issue(
                    CODE_UNRESOLVED_RESOURCE,
                    "Unresolved ExtResource(\"$id\") at line $lineNumber: no [ext_resource] declares id=\"$id\"",
                    ValidationSeverity.ERROR,
                )
            }
        }
        SUB_RESOURCE_REF.findAll(line).forEach { match ->
            val id = match.groupValues[1]
            if (id !in subResourceIds) {
                issues += issue(
                    CODE_UNRESOLVED_RESOURCE,
                    "Unresolved SubResource(\"$id\") at line $lineNumber: no [sub_resource] declares id=\"$id\"",
                    ValidationSeverity.ERROR,
                )
            }
        }
    }

    /**
     * Registers a node's path into [knownNodePaths]. Emits [CODE_BAD_PARENT] when a non-root node
     * references a parent path that has not yet been declared. [rootDeclared] tells whether a root
     * node has already been seen (the first node is the root).
     */
    private fun handleNode(
        header: Header,
        lineNumber: Int,
        rootDeclared: Boolean,
        knownNodePaths: MutableSet<String>,
        issues: MutableList<ValidationIssue>,
    ) {
        val name = header.attributes["name"]
        val parent = header.attributes["parent"]

        when {
            // First node is the root: no parent, or parent=".".
            !rootDeclared -> {
                if (parent != null && parent != ".") {
                    issues += issue(
                        CODE_BAD_PARENT,
                        "Root node at line $lineNumber declares parent=\"$parent\" but no parent is declared yet",
                        ValidationSeverity.ERROR,
                    )
                }
                knownNodePaths += "."
            }
            parent == null -> issues += issue(
                CODE_BAD_PARENT,
                "Node '${name ?: "?"}' at line $lineNumber has no parent attribute",
                ValidationSeverity.ERROR,
            )
            parent !in knownNodePaths -> issues += issue(
                CODE_BAD_PARENT,
                "Node '${name ?: "?"}' at line $lineNumber references undeclared parent \"$parent\"",
                ValidationSeverity.ERROR,
            )
            name != null -> knownNodePaths += if (parent == ".") name else "$parent/$name"
        }
    }

    private fun issue(code: String, message: String, severity: ValidationSeverity) =
        ValidationIssue(code = code, message = message, severity = severity)

    private data class Header(val type: String, val attributes: Map<String, String>)

    /**
     * Parses a section header line into its type and `key=value` attributes. Returns `null` when the
     * line is not a balanced, parseable header (e.g. unclosed `[`, missing type token).
     */
    private fun parseHeader(line: String): Header? {
        // The header must be a single balanced [...] pair.
        val balanced = line.startsWith("[") && line.endsWith("]") &&
            line.count { it == '[' } == 1 && line.count { it == ']' } == 1
        val tokens = line.takeIf { balanced }
            ?.substring(1, line.length - 1)
            ?.trim()
            ?.takeIf { it.isNotEmpty() }
            ?.let { tokenize(it) }
            ?: return null

        // The section type token must be a bare identifier, not a key=value pair.
        return tokens.firstOrNull()
            ?.takeIf { !it.contains('=') && it.all { ch -> ch.isLetterOrDigit() || ch == '_' } }
            ?.let { type -> parseAttributes(tokens.drop(1))?.let { Header(type, it) } }
    }

    /** Parses `key=value` attribute tokens. Returns `null` if any token is not a valid pair. */
    private fun parseAttributes(tokens: List<String>): Map<String, String>? {
        val pairs = tokens.map { token ->
            val eq = token.indexOf('=')
            val key = if (eq > 0) token.substring(0, eq) else ""
            if (key.isBlank()) {
                null
            } else {
                key to token.substring(eq + 1).trim().removeSurrounding("\"")
            }
        }
        return if (pairs.any { it == null }) null else pairs.filterNotNull().toMap()
    }

    /**
     * Splits header inner text into whitespace-separated tokens while respecting double-quoted
     * values (which may themselves contain spaces). Returns `null` if a quote is left unterminated.
     */
    private fun tokenize(inner: String): List<String>? {
        val tokens = mutableListOf<String>()
        val current = StringBuilder()
        var inQuotes = false
        for (ch in inner) {
            when {
                ch == '"' -> {
                    inQuotes = !inQuotes
                    current.append(ch)
                }
                ch.isWhitespace() && !inQuotes -> {
                    if (current.isNotEmpty()) {
                        tokens += current.toString()
                        current.clear()
                    }
                }
                else -> current.append(ch)
            }
        }
        if (inQuotes) return null
        if (current.isNotEmpty()) tokens += current.toString()
        return tokens
    }
}
