"""lfcs-sanitisation-verify@v1 — Layer 2 of the confidentiality seam.

Per Stage 4 § 2.2.4 — INDEPENDENT verification step that runs after Layer 1
(`sanitise.py`) and blocks output if any internal field is detected.

CRITICAL INDEPENDENCE PROPERTIES (do not violate):
1. This module does NOT import from `sanitise.py`.
2. This module maintains its OWN regex pattern set (intentionally re-derived
   from Stage 4 § 2.2.4 spec, not copied from Layer 1). If patterns drift
   between the two layers, drift is detected here.
3. This module reads from output artefact bytes / strings, not from any
   intermediate skill state.
4. This module is unconditional — Layer 1 cannot bypass it.
5. fail_closed=True by default. Caller flips to False only for diagnostic runs.

Returns a `VerificationResult` with pass/fail + list of leak findings.
The caller (orchestration script) MUST delete the target artefact + mark the
sanitisation log entry `verified_by_human=NEVER` if `passed is False`.
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from typing import Any


# Layer-2 INDEPENDENT regex set. Re-derived from Stage 4 § 2.2.4 (NOT copied from Layer 1).
# Some patterns intentionally differ in structure to catch coverage gaps in Layer 1.
_LAYER2_FORBIDDEN_PATTERNS: list[tuple[str, re.Pattern]] = [
    ("L2-internal-prefix", re.compile(r"_internal_", re.IGNORECASE)),
    ("L2-Internal-Table-prefix", re.compile(r"_Internal_[A-Z]")),
    ("L2-claude-token", re.compile(r"claude[\s\-_]?(?:code|opus|sonnet|haiku)", re.IGNORECASE)),
    ("L2-cowork-token", re.compile(r"cowork[\s\-_]", re.IGNORECASE)),
    ("L2-hermes-token", re.compile(r"\bhermes\b", re.IGNORECASE)),
    ("L2-openclaw-token", re.compile(r"\bopen?claw\b", re.IGNORECASE)),
    ("L2-source-tag", re.compile(r"\[source:\s*[^\]]+\]", re.IGNORECASE)),
    ("L2-agent-confidence-mention", re.compile(r"agent[\s\-_]+confidence", re.IGNORECASE)),
    ("L2-agent-rate-mention", re.compile(r"agent[\s\-_]+rate", re.IGNORECASE)),
    ("L2-placeholder-tag", re.compile(r"PLACEHOLDER", re.IGNORECASE)),
    ("L2-pending-derive-tag", re.compile(r"PENDING\s+DERIVE", re.IGNORECASE)),
    ("L2-friend-vault-mention", re.compile(r"friend[\s\-_]?vault", re.IGNORECASE)),
    ("L2-frontmatter-created-by", re.compile(r"^created_by\s*:", re.IGNORECASE | re.MULTILINE)),
    ("L2-prompt-ref-mention", re.compile(r"prompt_ref", re.IGNORECASE)),
]


@dataclass
class VerificationFinding:
    pattern_name: str
    snippet: str  # 80 chars around the match
    location: str  # file path / record path / cell coord


@dataclass
class VerificationResult:
    passed: bool
    findings: list[VerificationFinding] = field(default_factory=list)
    metadata_check: dict[str, Any] = field(default_factory=dict)
    notes: str = ""

    def to_audit_dict(self) -> dict:
        return {
            "layer2_pass": self.passed,
            "layer2_failures": "\n".join(
                f"{f.pattern_name} @ {f.location}: ...{f.snippet}..." for f in self.findings
            ),
        }


def verify_text(text: str, location: str = "<text>") -> VerificationResult:
    """Scan a text blob for any forbidden pattern. fail-closed on first finding (but reports all)."""
    findings: list[VerificationFinding] = []
    for name, pat in _LAYER2_FORBIDDEN_PATTERNS:
        for match in pat.finditer(text):
            start = max(0, match.start() - 40)
            end = min(len(text), match.end() + 40)
            findings.append(
                VerificationFinding(
                    pattern_name=name,
                    snippet=text[start:end].replace("\n", " "),
                    location=location,
                )
            )
    return VerificationResult(passed=len(findings) == 0, findings=findings)


def verify_record_set(records: list[dict], location: str = "<records>") -> VerificationResult:
    """Scan a list of dict records for `_internal_` keys + forbidden patterns in any string value."""
    findings: list[VerificationFinding] = []

    def _scan(obj: Any, path: str) -> None:
        if isinstance(obj, dict):
            for k, v in obj.items():
                if isinstance(k, str) and k.lower().startswith("_internal_"):
                    findings.append(
                        VerificationFinding(
                            pattern_name="L2-leaked-internal-key",
                            snippet=f"key={k!r}",
                            location=f"{path}/{k}",
                        )
                    )
                _scan(v, f"{path}/{k}")
        elif isinstance(obj, list):
            for i, v in enumerate(obj):
                _scan(v, f"{path}[{i}]")
        elif isinstance(obj, str):
            sub = verify_text(obj, location=path)
            findings.extend(sub.findings)

    for i, rec in enumerate(records):
        _scan(rec, f"{location}[{i}]")

    return VerificationResult(passed=len(findings) == 0, findings=findings)


def verify_xlsx(
    xlsx_path: str,
    expected_author: str,
    expected_company: str = "LF Construction Services Pty Ltd",
) -> VerificationResult:
    """Scan a sanitised xlsx file for forbidden tokens + verify file properties."""
    try:
        from openpyxl import load_workbook
    except ImportError as e:
        return VerificationResult(
            passed=False,
            notes=f"openpyxl unavailable ({e}); xlsx verify cannot run.",
        )

    findings: list[VerificationFinding] = []
    metadata_check: dict[str, Any] = {}

    wb = load_workbook(xlsx_path, data_only=False)

    # Sheet name forbidden-prefix check
    for sheet_name in wb.sheetnames:
        if sheet_name.startswith("_Internal_"):
            findings.append(
                VerificationFinding(
                    pattern_name="L2-leaked-internal-sheet",
                    snippet=sheet_name,
                    location=f"{xlsx_path}#sheet={sheet_name}",
                )
            )

    # Cell value + comment scan
    for ws in wb.worksheets:
        for row in ws.iter_rows():
            for cell in row:
                if isinstance(cell.value, str):
                    sub = verify_text(cell.value, location=f"{ws.title}!{cell.coordinate}")
                    findings.extend(sub.findings)
                if cell.comment is not None and cell.comment.text:
                    sub = verify_text(cell.comment.text, location=f"{ws.title}!{cell.coordinate}.comment")
                    findings.extend(sub.findings)

    # File properties check
    props = wb.properties
    metadata_check["author_actual"] = props.creator
    metadata_check["author_expected"] = expected_author
    metadata_check["last_modified_by_actual"] = props.lastModifiedBy
    # openpyxl DocumentProperties has no `company` attribute (extended property only).
    # Treat absence as not-present rather than mismatch.
    company_actual = getattr(props, "company", None)
    metadata_check["company_actual"] = company_actual
    metadata_check["company_expected"] = expected_company
    if (props.creator or "") != expected_author:
        findings.append(
            VerificationFinding(
                pattern_name="L2-author-mismatch",
                snippet=f"creator={props.creator!r} expected={expected_author!r}",
                location=f"{xlsx_path}#properties.creator",
            )
        )
    if (props.lastModifiedBy or "") != expected_author:
        findings.append(
            VerificationFinding(
                pattern_name="L2-lastmodifiedby-mismatch",
                snippet=f"lastModifiedBy={props.lastModifiedBy!r} expected={expected_author!r}",
                location=f"{xlsx_path}#properties.lastModifiedBy",
            )
        )
    if company_actual is not None and company_actual != expected_company:
        findings.append(
            VerificationFinding(
                pattern_name="L2-company-mismatch",
                snippet=f"company={company_actual!r} expected={expected_company!r}",
                location=f"{xlsx_path}#properties.company",
            )
        )

    return VerificationResult(
        passed=len(findings) == 0,
        findings=findings,
        metadata_check=metadata_check,
    )
