package com.correx.core.tools.compression

import com.correx.core.tools.compression.CompressionRule.CollapseConsecutiveDuplicates
import com.correx.core.tools.compression.CompressionRule.DropMatching
import com.correx.core.tools.compression.CompressionRule.HeadTail
import com.correx.core.tools.compression.CompressionRule.StripBlankLines
import com.correx.core.tools.compression.CompressionRule.StripLeadingWhitespace

/**
 * Applies an [OutputCompressionSpec] as an ordered, line-oriented pipeline.
 *
 * `DropMatching` regexes are compiled once at construction: an invalid pattern
 * fails at startup, never mid-run. [compress] never throws on input.
 */
class DeclarativeCompressor(private val spec: OutputCompressionSpec) : ToolOutputCompressor {

    /** Compiled regexes, indexed by the rule's position among [DropMatching] rules. */
    private val compiledPatterns: Map<DropMatching, Regex> =
        spec.rules.filterIsInstance<DropMatching>().associateWith { Regex(it.pattern) }

    override fun compress(raw: String, ctx: ToolOutputContext): String {
        if (spec.bypassOnError && ctx.exitCode != 0) return raw
        if (spec.rules.isEmpty()) return raw

        var lines = raw.split("\n")
        for (rule in spec.rules) {
            lines = applyRule(rule, lines)
        }
        return lines.joinToString("\n")
    }

    private fun applyRule(rule: CompressionRule, lines: List<String>): List<String> = when (rule) {
        StripBlankLines -> lines.filter { it.isNotBlank() }
        StripLeadingWhitespace -> lines.map { it.trimStart() }
        is DropMatching -> compiledPatterns[rule]
            ?.let { regex -> lines.filterNot { regex.containsMatchIn(it) } }
            ?: lines
        is HeadTail -> applyHeadTail(rule, lines)
        CollapseConsecutiveDuplicates -> collapseConsecutive(lines)
    }

    private fun applyHeadTail(rule: HeadTail, lines: List<String>): List<String> {
        if (lines.size <= rule.head + rule.tail) return lines
        val middle = lines.subList(rule.head, lines.size - rule.tail)
        val salienceRegex = rule.salience?.let { Regex(it, RegexOption.IGNORE_CASE) }
        val middleOut = if (salienceRegex == null) {
            listOf("… ${middle.size} lines elided …")
        } else {
            renderSalientMiddle(middle, salienceRegex, rule.salienceCap)
        }
        return lines.take(rule.head) + middleOut + lines.takeLast(rule.tail)
    }

    // Walks the elided middle, keeping order and retaining up to [cap] salient lines in place;
    // runs of dropped lines around them collapse into a single elision marker each, so a decisive
    // error at line 250 of 500 survives even though the bulk of the log is still bounded.
    private fun renderSalientMiddle(middle: List<String>, salienceRegex: Regex, cap: Int): List<String> {
        val out = ArrayList<String>()
        var elidedCount = 0
        var keptSalient = 0
        for (line in middle) {
            val isSalient = keptSalient < cap && salienceRegex.containsMatchIn(line)
            if (isSalient) {
                if (elidedCount > 0) {
                    out += "… $elidedCount lines elided …"
                    elidedCount = 0
                }
                out += line
                keptSalient++
            } else {
                elidedCount++
            }
        }
        if (elidedCount > 0) out += "… $elidedCount lines elided …"
        return out
    }

    private fun collapseConsecutive(lines: List<String>): List<String> {
        if (lines.isEmpty()) return lines
        val out = ArrayList<String>(lines.size)
        var prev = lines.first()
        var count = 1
        for (line in lines.drop(1)) {
            if (line == prev) {
                count++
            } else {
                out += render(prev, count)
                prev = line
                count = 1
            }
        }
        out += render(prev, count)
        return out
    }

    private fun render(line: String, count: Int): String =
        if (count > 1) "$line (×$count)" else line
}
