import WireProtocol

/// T-iOS-23 · OSC-title sanitiser. Terminal titles (OSC 0/2, surfaced by
/// SwiftTerm's `setTerminalTitle` delegate) are HOST/ATTACKER-CONTROLLED input
/// — they must pass through here before any list/registry/UI use (plan §7).
///
/// Rules, in order:
/// 1. Strip ALL Unicode control characters (C0 + C1 + DEL). SwiftTerm's OSC
///    parsing should already exclude C0, but that is NOT assumed — escape
///    injection gets zero tolerance at this boundary.
/// 2. Strip the spoofing vectors OSC parsing does NOT touch: zero-width and
///    bidirectional formatting characters — U+200B–200F (zero-width space /
///    non-joiner / joiner, LRM/RLM), U+202A–202E (bidi embeddings + the
///    classic RLO override), U+2066–2069 (bidi isolates), plus U+FEFF
///    (zero-width no-break space / BOM). Note: stripping U+200D (ZWJ) splits
///    multi-person emoji into their parts — safety over ligature aesthetics.
/// 3. Trim leading/trailing whitespace (mirrors web `title.trim() || null`,
///    public/tabs.ts:626 — the caller treats "" as "no title").
/// 4. Truncate to `Tunables.titleMaxLength` CHARACTERS (grapheme clusters —
///    an emoji flood caps at 256 visible glyphs and no glyph is ever split).
///
/// Pure + idempotent: sanitize(sanitize(x)) == sanitize(x).
public enum TitleSanitizer {
    /// Zero-width / bidi scalar ranges stripped on top of the control set.
    private static let strippedScalarRanges: [ClosedRange<UInt32>] = [
        0x200B...0x200F, // zero-width space/non-joiner/joiner, LRM, RLM
        0x202A...0x202E, // bidi embeddings, pops and overrides (incl. RLO)
        0x2066...0x2069, // bidi isolates (LRI/RLI/FSI/PDI)
        0xFEFF...0xFEFF, // zero-width no-break space / BOM
    ]

    public static func sanitize(_ raw: String) -> String {
        let kept = raw.unicodeScalars.filter { !isDisallowed($0) }
        let trimmed = String(String.UnicodeScalarView(kept))
            .trimmingCharacters(in: .whitespacesAndNewlines)
        return String(trimmed.prefix(Tunables.titleMaxLength))
    }

    private static func isDisallowed(_ scalar: Unicode.Scalar) -> Bool {
        // generalCategory .control covers C0 (U+0000–001F), DEL (U+007F)
        // and C1 (U+0080–009F) in one authoritative check.
        if scalar.properties.generalCategory == .control { return true }
        return strippedScalarRanges.contains { $0.contains(scalar.value) }
    }
}
