from __future__ import annotations

import json
import re
from collections import Counter
from datetime import datetime
from typing import Any

from .provider import call_chat


SPEAKER_RE = re.compile(
    r"^(?:\[(?P<ts>[^\]]{3,60})\]\s*)?(?P<speaker>[A-Za-zÀ-ÿ0-9_ .’'()\-]{1,60})\s*(?::|—|–|\|)\s*(?P<text>.+)$"
)
TIME_SKIP_RE = re.compile(
    r"\b(next morning|next day|that night|later that day|the following day|\d+\s+(?:hours?|days?|weeks?|months?|years?)\s+later|weeks? later|months? later|years? later)\b",
    re.I,
)
REL_PATTERNS = [
    (r"\b(ex[- ]?wife|ex[- ]?husband|exes?)\b", "Exes"),
    (r"\b(divorc(?:ed|e)|divorce)\b", "Divorced"),
    (r"\b(separat(?:ed|ion))\b", "Separated"),
    (r"\b(wife|husband|spouse|married|marriage)\b", "Married"),
    (r"\b(fianc(?:e|é|ée)|engaged|engagement)\b", "Engaged"),
    (r"\b(girlfriend|boyfriend|dating|couple)\b", "Dating"),
    (r"\b(best friends?|close friends?|friends?)\b", "Friends"),
    (r"\b(enemies|enemy|rivals?)\b", "Enemies"),
]

COMMON_LOCATION_RE = re.compile(r"\b(?:in|at|inside|into|back to|back at)\s+(?:the\s+)?(kitchen|bedroom|living room|bathroom|garage|office|hospital|school|classroom|base|headquarters|hq|restaurant|cafe|café|bar|hotel|home|house|apartment|flat|car|truck|garden|yard|beach|airport|station)\b", re.I)
FAMILY_FACT_RE = re.compile(r"\b(?:our|my|the)\s+(?:kids?|children|daughter|son|baby|babies|twins?)\b", re.I)
OTHER_FACT_RE = re.compile(r"\b(?:i work as|i work at|my job is|our dog|our cat|my dog|my cat|we live in|we live at)\b", re.I)

PERSONALITY_SIGNALS = [
    ("affectionate", re.compile(r"\b(?:i love you|love you|miss you|missed you|darling|sweetheart|babe|baby|my love|wife|husband)\b|\*(?:kiss(?:es|ed|ing)?|hug(?:s|ged|ging)?|cuddle(?:s|d|ing)?|nuzzle(?:s|d|ing)?)\b", re.I)),
    ("playful / teasing", re.compile(r"\b(?:teas(?:e|es|ed|ing)|kidding|joking|gotcha)\b|\*(?:grin(?:s|ned|ning)?|smirk(?:s|ed|ing)?|wink(?:s|ed|ing)?|laugh(?:s|ed|ing)?)\b", re.I)),
    ("protective", re.compile(r"\b(?:are you (?:okay|alright)|stay (?:behind|close)|be careful|keep close|i(?:'ll| will) protect|you(?:'re| are) safe|not letting .* hurt|won't let .* hurt)\b", re.I)),
    ("gentle / reassuring", re.compile(r"\b(?:it'?s okay|i'?m here|take your time|no rush|you(?:'re| are) safe|we(?:'ll| will) figure|breathe,? i'?m here)\b", re.I)),
    ("repair-oriented", re.compile(r"\b(?:i'?m sorry|i am sorry|my fault|i was wrong|forgive me|can we talk|let'?s talk this through)\b", re.I)),
    ("direct / assertive", re.compile(r"\b(?:look at me|listen to me|tell me the truth|stay with me|come here|we need to talk)\b", re.I)),
    ("emotionally expressive", re.compile(r"\b(?:i love|i miss|i'?m scared|i am scared|i'?m afraid|i am afraid|i'?m worried|i am worried|i hate that|i need you)\b|\*(?:cry|cries|cried|laugh|laughs|laughed|smile|smiles|smiled|shak(?:e|es|ing))\b", re.I)),
]

CONFLICT_PATTERNS = [
    ("tries to repair after conflict", re.compile(r"\b(?:i'?m sorry|i am sorry|my fault|i was wrong|forgive me|can we talk|let'?s talk this through)\b", re.I)),
    ("de-escalates / reassures", re.compile(r"\b(?:calm down|it'?s okay|we don'?t have to fight|take your time|let'?s slow down|i'?m here)\b", re.I)),
    ("withdraws when overwhelmed", re.compile(r"\b(?:leave me alone|i need space|give me a minute|i need a minute)\b|\*(?:walks away|goes quiet|falls silent|steps away)\b", re.I)),
    ("can become openly confrontational", re.compile(r"\*(?:snaps|shouts|yells|glares)\b|\b(?:don'?t lie to me|enough!|stop it!)\b", re.I)),
]

ACTION_RE = re.compile(r"\*([^*\n]{2,90})\*")
MANNERISM_WORDS = re.compile(r"\b(smiles?|grins?|smirks?|laughs?|sighs?|shrugs?|nods?|raises? (?:an? )?eyebrow|folds? (?:his|her|their) arms|rubs? (?:his|her|their) (?:neck|face|jaw)|runs? (?:a )?hand through (?:his|her|their) hair|tilts? (?:his|her|their) head)\b", re.I)

DATE_FORMATS = (
    "%Y-%m-%d %H:%M", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M:%S.%f",
    "%Y-%m-%d", "%d/%m/%Y %H:%M", "%d/%m/%Y", "%m/%d/%Y %H:%M", "%m/%d/%Y",
)


def _clean_text(v: Any) -> str:
    if v is None:
        return ""
    if isinstance(v, list):
        return "\n".join(str(x) for x in v if x is not None).strip()
    return str(v).strip()


def _generic_json_message(obj: dict[str, Any]) -> dict[str, Any] | None:
    content = obj.get("content")
    if isinstance(content, dict):
        content = content.get("parts") or content.get("text") or content.get("content")
    text = _clean_text(content or obj.get("text") or obj.get("mes") or obj.get("message") or obj.get("body"))
    if not text:
        return None
    role = str(obj.get("role") or "").lower()
    speaker = _clean_text(obj.get("name") or obj.get("speaker") or obj.get("username") or obj.get("author"))
    author = obj.get("author")
    if isinstance(author, dict):
        role = str(author.get("role") or role).lower()
        speaker = _clean_text(author.get("name") or speaker)
    if not speaker:
        speaker = {"user": "You", "assistant": "AI", "system": "System"}.get(role, "Imported speaker")
    is_user = bool(obj.get("is_user")) or role == "user"
    timestamp = _clean_text(obj.get("timestamp") or obj.get("send_date") or obj.get("created_at") or obj.get("create_time"))
    return {"speaker": speaker, "text": text, "timestamp": timestamp, "role_hint": "user" if is_user else role}


def _flatten_chatgpt_mapping(root: Any) -> list[dict[str, Any]]:
    conversations = root if isinstance(root, list) else [root]
    out: list[tuple[float, dict[str, Any]]] = []
    for conv in conversations:
        if not isinstance(conv, dict) or not isinstance(conv.get("mapping"), dict):
            continue
        for node in conv["mapping"].values():
            if not isinstance(node, dict):
                continue
            msg = node.get("message")
            if not isinstance(msg, dict):
                continue
            parsed = _generic_json_message(msg)
            if not parsed:
                continue
            t = msg.get("create_time") or node.get("create_time") or 0
            try:
                order = float(t or 0)
            except Exception:
                order = 0.0
            out.append((order, parsed))
    out.sort(key=lambda x: x[0])
    return [x[1] for x in out]


def _parse_json(raw: str) -> tuple[list[dict[str, Any]], str] | None:
    try:
        obj = json.loads(raw)
    except Exception:
        return None
    mapped = _flatten_chatgpt_mapping(obj)
    if mapped:
        return mapped, "Chat export JSON"
    items: list[Any]
    if isinstance(obj, list):
        items = obj
    elif isinstance(obj, dict):
        for key in ("messages", "chat", "history", "conversation"):
            if isinstance(obj.get(key), list):
                items = obj[key]
                break
        else:
            items = [obj]
    else:
        return None
    parsed = [p for x in items if isinstance(x, dict) for p in [_generic_json_message(x)] if p]
    return (parsed, "JSON messages") if parsed else None


def _parse_jsonl(raw: str) -> tuple[list[dict[str, Any]], str] | None:
    rows = []
    for line in raw.splitlines():
        line = line.strip()
        if not line.startswith("{"):
            continue
        try:
            obj = json.loads(line)
        except Exception:
            continue
        if isinstance(obj, dict):
            msg = _generic_json_message(obj)
            if msg:
                rows.append(msg)
    return (rows, "JSONL messages") if len(rows) >= 2 else None


def _parse_text(raw: str) -> tuple[list[dict[str, Any]], str]:
    messages: list[dict[str, Any]] = []
    current: dict[str, Any] | None = None
    for line in raw.replace("\r\n", "\n").replace("\r", "\n").split("\n"):
        stripped = line.strip()
        if not stripped:
            if current and current["text"] and not current["text"].endswith("\n"):
                current["text"] += "\n"
            continue
        m = SPEAKER_RE.match(stripped)
        if m and len(m.group("speaker").split()) <= 8:
            if current:
                current["text"] = current["text"].strip()
                messages.append(current)
            speaker = m.group("speaker").strip()
            role_hint = "user" if speaker.lower() in {"you", "user", "me", "human"} else ""
            current = {"speaker": speaker, "text": m.group("text").strip(), "timestamp": (m.group("ts") or "").strip(), "role_hint": role_hint}
        elif current:
            current["text"] += ("\n" if current["text"] else "") + stripped
    if current:
        current["text"] = current["text"].strip()
        messages.append(current)
    if len(messages) >= 2:
        return messages, "Speaker-labelled text"
    paragraphs = [p.strip() for p in re.split(r"\n\s*\n", raw) if p.strip()]
    if not paragraphs:
        return [], "Empty"
    # Preserve unstructured imports as evidence instead of inventing speaker alternation.
    return [{"speaker": "Imported transcript", "text": "\n\n".join(paragraphs), "timestamp": "", "role_hint": "assistant"}], "Unstructured text"


def parse_transcript(raw: str, platform: str = "Other") -> dict[str, Any]:
    raw = (raw or "").lstrip("\ufeff").strip()
    if not raw:
        return {"format": "Empty", "messages": [], "speakers": [], "warnings": ["No transcript text was supplied."]}
    parsed = _parse_json(raw) or _parse_jsonl(raw)
    if parsed:
        messages, fmt = parsed
    else:
        messages, fmt = _parse_text(raw)
    counts = Counter(m["speaker"] for m in messages if m.get("speaker"))
    speakers = [{"name": k, "count": v} for k, v in counts.most_common()]
    warnings: list[str] = []
    if fmt == "Unstructured text":
        warnings.append("Speaker labels were not detected. The full transcript is still preserved, but message-by-message reconstruction will be limited.")
    if len(raw) > 2_000_000:
        warnings.append("This is a very large import. The full text is preserved; analysis may take longer.")
    return {
        "format": fmt,
        "platform": platform or "Other",
        "messages": messages,
        "message_count": len(messages),
        "speakers": speakers,
        "warnings": warnings,
        "preview": messages[:8],
    }


def _last_timestamp(messages: list[dict[str, Any]]) -> tuple[str | None, str | None]:
    for m in reversed(messages):
        raw = str(m.get("timestamp") or "").strip()
        if not raw:
            continue
        if raw.replace(".", "", 1).isdigit():
            try:
                dt = datetime.fromtimestamp(float(raw))
                return dt.strftime("%Y-%m-%d"), dt.strftime("%H:%M")
            except Exception:
                pass
        iso = raw.replace("Z", "+00:00")
        try:
            dt = datetime.fromisoformat(iso)
            return dt.strftime("%Y-%m-%d"), dt.strftime("%H:%M")
        except Exception:
            pass
        for fmt in DATE_FORMATS:
            try:
                dt = datetime.strptime(raw[:19], fmt)
                return dt.strftime("%Y-%m-%d"), dt.strftime("%H:%M") if "%H" in fmt else None
            except Exception:
                continue
    return None, None


def _relationship_candidate(messages: list[dict[str, Any]]) -> dict[str, Any] | None:
    # Latest explicit wording wins, matching MelumiyaChat's current-state-over-old-context rule.
    for m in reversed(messages):
        text = m.get("text", "")
        for pattern, status in REL_PATTERNS:
            hit = re.search(pattern, text, re.I)
            if hit:
                evidence = re.sub(r"\s+", " ", text).strip()
                return {"status": status, "confidence": 0.68, "evidence": evidence[:360], "speaker": m.get("speaker", "")}
    return None


def _timeline_candidates(messages: list[dict[str, Any]], limit: int = 30) -> list[dict[str, Any]]:
    found: list[dict[str, Any]] = []
    seen: set[str] = set()
    for m in messages:
        text = m.get("text", "")
        hit = TIME_SKIP_RE.search(text)
        if not hit:
            continue
        event = re.sub(r"\s+", " ", text).strip()[:420]
        key = event.lower()
        if key in seen:
            continue
        seen.add(key)
        found.append({
            "event": event,
            "date": m.get("timestamp") or hit.group(0),
            "participants": m.get("speaker") or "",
            "source": "Imported chat",
            "confidence": 0.72,
        })
        if len(found) >= limit:
            break
    return found


def _current_location(messages: list[dict[str, Any]]) -> dict[str, Any] | None:
    for m in reversed(messages):
        hit = COMMON_LOCATION_RE.search(m.get("text", ""))
        if hit:
            return {"name": hit.group(1).title(), "evidence": re.sub(r"\s+", " ", m.get("text", ""))[:320], "confidence": 0.74}
    return None


def _custom_fact_candidates(messages: list[dict[str, Any]], limit: int = 16) -> list[dict[str, Any]]:
    out: list[dict[str, Any]] = []
    seen: set[str] = set()
    for m in messages:
        text = re.sub(r"\s+", " ", m.get("text", "")).strip()
        if not text:
            continue
        category = None
        if FAMILY_FACT_RE.search(text):
            category = "Family fact"
        elif OTHER_FACT_RE.search(text):
            category = "Life fact"
        if not category:
            continue
        value = text[:360]
        key = value.lower()
        if key in seen:
            continue
        seen.add(key)
        out.append({"category": category, "label": "Imported fact", "value": value, "source": "Imported chat", "confidence": 0.66})
        if len(out) >= limit:
            break
    return out


def _evidence_excerpt(text: str, limit: int = 220) -> str:
    return re.sub(r"\s+", " ", str(text or "")).strip()[:limit]


def _personality_profiles(messages: list[dict[str, Any]], user_speaker: str) -> list[dict[str, Any]]:
    """Build cautious personality drafts from repeated observed behaviour across the whole import.

    This is interpretation, not hard canon. Every profile carries evidence/confidence and stays editable.
    """
    user_key = (user_speaker or "").strip().lower()
    grouped: dict[str, list[dict[str, Any]]] = {}
    for m in messages:
        name = str(m.get("speaker") or "").strip()
        if not name or name.lower() in {user_key, "you", "user", "me", "human", "system", "imported transcript"}:
            continue
        grouped.setdefault(name, []).append(m)

    profiles: list[dict[str, Any]] = []
    for name, rows in grouped.items():
        texts = [str(x.get("text") or "") for x in rows if str(x.get("text") or "").strip()]
        if not texts:
            continue
        word_counts = [len(re.findall(r"\b\w+\b", t)) for t in texts]
        avg_words = sum(word_counts) / max(1, len(word_counts))
        question_ratio = sum("?" in t for t in texts) / len(texts)
        exclaim_ratio = sum("!" in t for t in texts) / len(texts)
        action_ratio = sum(bool(ACTION_RE.search(t)) for t in texts) / len(texts)

        style_bits = []
        if avg_words <= 10:
            style_bits.append("usually concise / economical")
        elif avg_words >= 34:
            style_bits.append("often detailed / expansive")
        else:
            style_bits.append("moderate-length replies")
        if question_ratio >= 0.30:
            style_bits.append("frequently asks questions")
        if exclaim_ratio >= 0.30:
            style_bits.append("often emphatic")
        if action_ratio >= 0.35:
            style_bits.append("frequently communicates through physical actions as well as dialogue")

        traits: list[str] = []
        evidence: list[dict[str, Any]] = []
        signal_counts: dict[str, int] = {}
        for label, pattern in PERSONALITY_SIGNALS:
            hits = []
            for t in texts:
                if pattern.search(t):
                    hits.append(_evidence_excerpt(t))
            if hits:
                signal_counts[label] = len(hits)
                # Repeated behaviour gets treated as a stronger personality signal. A one-off is still
                # retained as low-confidence evidence, especially for short imports.
                if len(hits) >= 2 or len(texts) <= 5:
                    traits.append(label)
                evidence.append({
                    "signal": label,
                    "count": len(hits),
                    "excerpt": hits[-1],
                    "confidence": round(min(0.88, 0.42 + min(len(hits), 5) * 0.09 + min(len(texts), 40) * 0.004), 2),
                })

        conflict = []
        for label, pattern in CONFLICT_PATTERNS:
            hits = [t for t in texts if pattern.search(t)]
            if len(hits) >= 2 or (hits and len(texts) <= 5):
                conflict.append(label)
                evidence.append({"signal": f"conflict: {label}", "count": len(hits), "excerpt": _evidence_excerpt(hits[-1]), "confidence": 0.5 if len(hits)==1 else min(0.84, 0.56 + len(hits)*0.07)})

        # Mannerisms are only promoted when actually observed in action text.
        manner_counter: Counter[str] = Counter()
        manner_example: dict[str, str] = {}
        for t in texts:
            for action in ACTION_RE.findall(t):
                hit = MANNERISM_WORDS.search(action)
                if hit:
                    key = hit.group(1).lower().strip()
                    manner_counter[key] += 1
                    manner_example.setdefault(key, _evidence_excerpt(f"*{action}*"))
        mannerisms = []
        for key, count in manner_counter.most_common(6):
            if count >= 2 or len(texts) <= 5:
                mannerisms.append(key)
                evidence.append({"signal": f"mannerism: {key}", "count": count, "excerpt": manner_example[key], "confidence": 0.5 if count==1 else min(0.86, 0.58 + count*0.06)})

        affection_bits = [x for x in ("affectionate", "playful / teasing", "protective", "gentle / reassuring") if x in signal_counts]
        affection_style = ", ".join(affection_bits) if affection_bits else "Not enough repeated evidence yet"
        if "emotionally expressive" in signal_counts:
            emotional_expression = "Open emotional expression appears in the imported scenes"
        elif avg_words <= 10 and not affection_bits:
            emotional_expression = "Observed dialogue is restrained/concise; deeper emotional style is still uncertain"
        else:
            emotional_expression = "Not enough repeated evidence yet"
        relationship_specific = ""
        if affection_bits and user_speaker:
            relationship_specific = f"With {user_speaker}: " + ", ".join(affection_bits) + " in the imported scenes"

        profile_confidence = round(min(0.88, 0.42 + min(len(texts), 60) * 0.006 + min(sum(1 for x in evidence if x.get('count',0)>=2), 5) * 0.04), 2)
        profiles.append({
            "name": name,
            "personality": traits,
            "speech_style": "; ".join(style_bits),
            "affection_style": affection_style,
            "conflict_style": "; ".join(conflict) if conflict else "Not enough repeated conflict evidence yet",
            "emotional_expression": emotional_expression,
            "mannerisms": mannerisms,
            "relationship_specific": relationship_specific,
            "personality_source": "Imported chat — whole-transcript local personality scan",
            "personality_confidence": profile_confidence,
            "personality_evidence": evidence[:14],
            "personality_status": "editable draft reconstructed from observed behaviour",
            "message_sample_size": len(texts),
        })
    return profiles


def heuristic_analysis(parsed: dict[str, Any], user_speaker: str, persona_name: str) -> dict[str, Any]:
    messages = parsed.get("messages") or []
    normalized_user = (user_speaker or persona_name or "You").strip()
    speakers = [x["name"] for x in parsed.get("speakers", [])]
    others = [s for s in speakers if s.lower() != normalized_user.lower() and s.lower() not in {"you", "user", "me", "human", "system"}]
    personality_profiles = _personality_profiles(messages, normalized_user)
    profile_by_name = {x["name"].lower(): x for x in personality_profiles}
    world_characters = []
    for name in others:
        character = {
            "name": name,
            "role": "Imported RP participant",
            "summary": f"Appears in {next((x['count'] for x in parsed.get('speakers', []) if x['name']==name), 0)} imported messages.",
            "source": "Imported chat",
            "confidence": 0.95,
        }
        profile = profile_by_name.get(name.lower())
        if profile:
            character.update({k: v for k, v in profile.items() if k != "name"})
        world_characters.append(character)
    recent_names: list[str] = []
    for m in messages[-14:]:
        name = str(m.get("speaker") or "").strip()
        if name and name.lower() != normalized_user.lower() and name not in recent_names and name.lower() not in {"system", "imported transcript"}:
            recent_names.append(name)
    rel = _relationship_candidate(messages)
    date, time = _last_timestamp(messages)
    timeline = _timeline_candidates(messages)
    location = _current_location(messages)
    custom_entries = _custom_fact_candidates(messages)
    review: list[dict[str, Any]] = []
    if rel:
        review.append({"kind": "relationship", "value": rel["status"], "confidence": rel["confidence"], "evidence": rel["evidence"], "status": "auto-applied; editable"})
    for x in timeline[:10]:
        review.append({"kind": "timeline", "value": x["date"], "confidence": x["confidence"], "evidence": x["event"], "status": "auto-added; editable"})
    if location:
        review.append({"kind": "current location", "value": location["name"], "confidence": location["confidence"], "evidence": location["evidence"], "status": "auto-applied; editable"})
    for x in custom_entries[:8]:
        review.append({"kind": x["category"], "value": x["value"], "confidence": x["confidence"], "evidence": x["value"], "status": "auto-added; editable"})
    for profile in personality_profiles:
        trait_text = ", ".join(profile.get("personality") or []) or profile.get("speech_style") or "Personality draft"
        first_ev = next((e.get("excerpt") for e in profile.get("personality_evidence") or [] if e.get("excerpt")), "Observed across imported messages")
        review.append({"kind": f"personality: {profile['name']}", "value": trait_text, "confidence": profile.get("personality_confidence", 0.5), "evidence": first_ev, "status": "auto-filled personality draft; editable"})
    hard_notes = [
        "Imported transcript is preserved as source evidence.",
        "Facts extracted from an import remain editable; user corrections outrank extraction.",
        "Major irreversible events still require explicit user approval unless clearly established in the imported history.",
    ]
    return {
        "engine": "whole-transcript local scan",
        "relationship_status": rel["status"] if rel else None,
        "relationship_evidence": rel,
        "characters": world_characters,
        "personality_profiles": personality_profiles,
        "scene": {
            "date": date,
            "time": time,
            "present": recent_names[:8],
            "controlled_character": persona_name or normalized_user,
            "location": location["name"] if location else "Imported RP — review current location",
        },
        "timeline": timeline,
        "custom_entries": custom_entries,
        "locations": ([{"name": location["name"], "description": "Detected from the latest imported scene.", "source": "Imported chat", "confidence": location["confidence"]}] if location else []),
        "knowledge_facts": [],
        "physical_notes": [],
        "hard_canon_notes": hard_notes,
        "review": review,
        "stats": {"messages": len(messages), "speakers": len(speakers), "characters": len(world_characters)},
    }


DEEP_SYSTEM = """You are MelumiyaChat's import analyst. Extract structured RP facts and observed character behaviour from imported transcript excerpts.
Return ONLY JSON with keys characters, personality_profiles, relationships, locations, timeline, secrets, physical, current_scene, memories.
For personality_profiles, return one object per recurring non-user character when evidence supports it, with: name, personality (list of observed traits), speech_style, affection_style, conflict_style, emotional_expression, mannerisms (list), public_private, relationship_specific, evidence, confidence. Distinguish repeated behaviour from one-off reactions. A character being angry once does NOT make them an aggressive person. Do not infer public/private differences unless the excerpts actually show both contexts.
Never invent missing facts. Preserve uncertainty. User statements outrank assistant inference. Old relationship states may be historical; do not automatically treat them as current. Every item should contain a short evidence field and confidence 0..1 when possible. Do not quote long passages; evidence must be brief.
"""


def deep_analysis(parsed: dict[str, Any], provider: str, model: str | None, user_speaker: str, persona_name: str) -> dict[str, Any] | None:
    if provider == "mock" or not (parsed.get("messages") or []):
        return None
    messages = parsed["messages"]
    # The deterministic pass always scans the whole import. The AI pass adds richer interpretation from distributed excerpts.
    total = len(messages)
    if total <= 90:
        chosen = messages
    else:
        idxs = sorted(set(list(range(0, min(30, total))) + list(range(max(0, total//2-15), min(total, total//2+15))) + list(range(max(0, total-40), total))))
        chosen = [messages[i] for i in idxs]
    transcript = "\n".join(f"{m.get('speaker','?')}: {m.get('text','')}" for m in chosen)
    prompt = f"USER-CONTROLLED SPEAKER: {user_speaker or persona_name}\nPERSONA NAME: {persona_name}\n\nTRANSCRIPT EXCERPTS:\n{transcript[:70000]}"
    try:
        result = call_chat(provider, model, [{"role": "system", "content": DEEP_SYSTEM}, {"role": "user", "content": prompt}], temperature=0.1, max_tokens=2600)
        raw = result["content"].strip()
        raw = re.sub(r"^```(?:json)?\s*|\s*```$", "", raw, flags=re.I | re.S)
        obj = json.loads(raw)
        return obj if isinstance(obj, dict) else None
    except Exception:
        return None


def merge_analysis(base: dict[str, Any], deep: dict[str, Any] | None) -> dict[str, Any]:
    if not deep:
        return base
    out = dict(base)
    out["engine"] = "whole-transcript local scan + AI enrichment"
    # AI additions go into suggestion buckets so they never silently overrule a user's imported facts.
    out["ai_suggestions"] = deep
    review = list(out.get("review") or [])
    for key in ("personality_profiles", "relationships", "locations", "timeline", "secrets", "physical", "current_scene", "memories"):
        value = deep.get(key)
        if value:
            review.append({"kind": f"AI suggestion: {key}", "value": "Review available", "confidence": 0.65, "evidence": "AI-assisted import analysis", "status": "suggestion only"})
    out["review"] = review
    return out
