#!/usr/bin/env python3
"""Probe a transcript with the meeting-minutes evidence classifier.

This is a diagnostic runner. It does not write minutes; it ranks transcript
segments that look like action, document-control, regulatory, process, decision,
risk or discussion evidence.
"""
from __future__ import annotations

import argparse
import json
import re
from pathlib import Path

import joblib
import numpy as np
from sentence_transformers import SentenceTransformer


TURN_RE = re.compile(r"^\s*(?:\d{1,2}:\d{2}(?::\d{2})?\s+)?([A-Z][A-Za-z .'\-]{1,60}):\s+(.+)$")
TEAMS_TURN_RE = re.compile(r"^\s*([A-Z][A-Za-z .,'\-]{1,70})\s+(\d{1,2}:\d{2}(?::\d{2})?)(.+)$")
SENTENCE_RE = re.compile(r"(?<=[.!?])\s+")
ACTION_RE = re.compile(
    r"\b(?:will|shall|to\s+(?:review|share|send|follow up|complete|update|provide|confirm|prepare|approve|sign off|generate|trace|identify|document)|"
    r"need(?:s)?\s+to|have\s+to|can\s+send|could\s+you|please|review|share|send|follow up|complete|update|provide|confirm|prepare|approve|sign off)\b",
    re.I,
)
EVIDENCE_RE = re.compile(
    r"\b(?:qms|manual|label|labels|doc|docs|declaration|conformity|hpra|invoice|fee|medenvoy|udimed|udi|mdr|ppe|audit|risk|assessment|plan|"
    r"standard|standards|change request|debug|test|testing|driver|tracker|sharepoint|code of conduct|attestation|packaging|warehouse|picked|packed|distribution)\b",
    re.I,
)
NOISE_RE = re.compile(r"^(?:yeah|yes|no|ok|okay|mm|mhm|right|thanks?|grand|lovely|brilliant|super|grace|fine|cool|sorry)[\s.!,]*$", re.I)


def compact(value: str) -> str:
    return re.sub(r"\s+", " ", str(value or "")).strip()


def candidate_hint(text: str) -> float:
    score = 0.0
    if ACTION_RE.search(text):
        score += 0.45
    if EVIDENCE_RE.search(text):
        score += 0.35
    if re.search(r"\b(?:by|before|after|next|today|tomorrow|wednesday|friday|week|24 march|17 june)\b", text, re.I):
        score += 0.15
    if re.search(r"\b[A-Z][a-z]+(?:-[A-Z][a-z]+)?\b", text):
        score += 0.05
    return score


def candidate_segments(text: str, max_chars: int = 420) -> list[dict]:
    segments: list[dict] = []
    current_speaker = ""
    for line_no, raw in enumerate(text.splitlines(), start=1):
        line = compact(raw)
        if not line:
            continue
        match = TURN_RE.match(line)
        if match:
            current_speaker = compact(match.group(1))
            body = compact(match.group(2))
        elif teams_match := TEAMS_TURN_RE.match(line):
            current_speaker = compact(teams_match.group(1))
            body = compact(teams_match.group(3))
        else:
            body = line
        if len(body) < 16:
            continue
        for sentence in SENTENCE_RE.split(body):
            sentence = compact(sentence)
            word_count = len(re.findall(r"[A-Za-z0-9]+", sentence))
            if len(sentence) < 16 or word_count < 4 or NOISE_RE.match(sentence):
                continue
            if len(sentence) > max_chars:
                sentence = sentence[: max_chars - 1].rstrip() + "..."
            segments.append({"line": line_no, "speaker": current_speaker, "text": sentence, "candidateHint": round(candidate_hint(sentence), 3)})
    return segments


def top_prob(classes, probs):
    idx = int(np.argmax(probs))
    return str(classes[idx]), float(probs[idx])


def classify(model_path: Path, segments: list[dict], threshold: float, limit: int) -> dict:
    bundle = joblib.load(model_path)
    embedder = SentenceTransformer(bundle["embedding_model"])
    texts = [segment["text"] for segment in segments]
    if not texts:
        return {"segments": [], "counts": {}, "top": []}
    X = embedder.encode(texts, normalize_embeddings=True, convert_to_numpy=True, batch_size=32, show_progress_bar=False)
    evidence_probs = bundle["status_classifier"].predict_proba(X)
    commitment_clf = bundle.get("action_state_classifier")
    commitment_le = bundle.get("action_state_label_encoder")
    commitment_probs = commitment_clf.predict_proba(X) if commitment_clf is not None and commitment_le is not None else None
    signal_clf = bundle.get("signal_classifier")
    signal_mlb = bundle.get("signal_binarizer")
    signal_probs = np.asarray(signal_clf.predict_proba(X)) if signal_clf is not None and signal_mlb is not None else None

    rows = []
    counts: dict[str, int] = {}
    for idx, segment in enumerate(segments):
        evidence_type, evidence_conf = top_prob(bundle["status_label_encoder"].classes_, evidence_probs[idx])
        if commitment_probs is not None:
            commitment_state, commitment_conf = top_prob(commitment_le.classes_, commitment_probs[idx])
        else:
            commitment_state, commitment_conf = "", 0.0
        signals = []
        if signal_probs is not None:
            order = np.argsort(-signal_probs[idx])
            for signal_idx in order[:8]:
                score = float(signal_probs[idx][signal_idx])
                if score >= threshold:
                    signals.append({"label": str(signal_mlb.classes_[signal_idx]), "score": round(score, 3)})
        counts[evidence_type] = counts.get(evidence_type, 0) + 1
        hint = float(segment.get("candidateHint", 0.0))
        rank_score = evidence_conf + hint
        if commitment_state == "confirmed_action":
            rank_score += 0.35
        if evidence_type in {"action_commitment", "document_control_task", "regulatory_obligation", "risk_dependency", "decision_agreement", "process_overview"}:
            rank_score += 0.15
        rows.append({
            **segment,
            "evidenceType": evidence_type,
            "evidenceConfidence": round(evidence_conf, 3),
            "commitmentState": commitment_state,
            "commitmentConfidence": round(commitment_conf, 3),
            "signals": signals,
            "rankScore": round(rank_score, 3),
        })
    rows.sort(key=lambda row: row["rankScore"], reverse=True)
    return {"counts": counts, "top": rows[:limit], "segmentsScored": len(rows)}


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--model", required=True)
    parser.add_argument("--text-file", required=True)
    parser.add_argument("--threshold", type=float, default=0.45)
    parser.add_argument("--limit", type=int, default=30)
    args = parser.parse_args()

    text = Path(args.text_file).read_text(encoding="utf-8", errors="replace")
    segments = candidate_segments(text)
    result = classify(Path(args.model), segments, args.threshold, args.limit)
    print(json.dumps(result, ensure_ascii=False, indent=2))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
