"""MCP tools for decision drafting support."""

from __future__ import annotations

import json
from pathlib import Path
from uuid import UUID

from legal_mcp import config
from legal_mcp.services import audit, db, embeddings, git_sync, research_md
from legal_mcp.services.lessons import (
    CITATION_GUIDANCE,
    DECISION_TEMPLATES,
    DISCUSSION_RULES,
    GOLDEN_RATIOS,
    OPENING_STRATEGIES,
    PARAGRAPH_LENGTHS,
    PRACTICE_AREA_OVERRIDES,
    SUMMARY_STRATEGIES,
    TRANSITION_PHRASES,
    VALID_OUTCOMES,
    canonical_outcome,
    format_ratios_comment,
    get_lessons_for_outcome,
)
from legal_mcp.tools.envelope import empty, err, ok  # GAP-48: SSoT envelope

# Fallback template for cases without expected_outcome
DECISION_TEMPLATE = """# החלטה

## בפני: דפנה תמיר, יו"ר ועדת הערר מחוז ירושלים

**ערר מספר:** {case_number}
**נושא:** {subject}
**העוררים:** {appellants}
**המשיבים:** {respondents}
**כתובת הנכס:** {property_address}

---

## א. רקע עובדתי

[תיאור הרקע העובדתי של הערר]

## ב. טענות העוררים

[סיכום טענות העוררים]

## ג. טענות המשיבים

[סיכום טענות המשיבים]

## ד. דיון והכרעה

[ניתוח משפטי]

## ה. מסקנה

[מסקנת הוועדה]

## ו. החלטה

[ההחלטה הסופית]

---
ניתנה היום, {date}
דפנה תמיר, יו"ר ועדת הערר
"""


async def get_style_guide() -> str:
    """שליפת דפוסי הסגנון של דפנה - נוסחאות, ביטויים אופייניים ומבנה, כולל לקחים מעשיים."""
    patterns = await db.get_style_patterns()

    result = "# מדריך סגנון - דפנה תמיר\n\n"

    # Part 1: DB-sourced patterns (from analyze_style)
    if patterns:
        type_names = {
            "opening_formula": "נוסחאות פתיחה",
            "transition": "ביטויי מעבר",
            "citation_style": "סגנון ציטוט",
            "analysis_structure": "מבנה ניתוח",
            "closing_formula": "נוסחאות סיום",
            "characteristic_phrase": "ביטויים אופייניים",
            "argument_flow": "זרימת טיעון",
            "evidence_handling": "התייחסות לראיות",
        }

        grouped: dict[str, list] = {}
        for p in patterns:
            pt = p["pattern_type"]
            if pt not in grouped:
                grouped[pt] = []
            grouped[pt].append({
                "text": p["pattern_text"],
                "context": p["context"],
                "frequency": p["frequency"],
            })

        for ptype, items in grouped.items():
            result += f"## {type_names.get(ptype, ptype)}\n\n"
            for item in items:
                result += f"- **{item['text']}** ({item['context']}, תדירות: {item['frequency']})\n"
            result += "\n"
    else:
        result += "_לא נמצאו דפוסים מקורפוס. יש להעלות החלטות ולהריץ /style-report._\n\n"

    # Part 2: Lessons-based guidance
    result += "---\n\n# לקחים מעשיים\n\n"

    # Universal discussion rules
    result += "## כללי דיון אוניברסליים\n\n"
    for rule in DISCUSSION_RULES["universal"]:
        result += f"- {rule}\n"
    result += "\n"

    # Citation technique
    result += "## טכניקת ציטוט\n\n"
    result += f"{CITATION_GUIDANCE}\n\n"

    # Transition phrases
    result += "## ביטויי מעבר (מהשוואת טיוטות)\n\n"
    for p in TRANSITION_PHRASES:
        ctx = p["context"]
        outcome_note = f" [בעיקר ב-{p['outcome']}]" if p["outcome"] else ""
        result += f"- **{p['phrase']}** — {ctx}{outcome_note}\n"
    result += "\n"

    # Paragraph lengths
    result += "## אורכי פסקאות מומלצים\n\n"
    result += "| סוג | מילים |\n|-----|------|\n"
    labels = {
        "claims": "טענות",
        "discussion_regular": "דיון רגיל",
        "discussion_with_citation": "דיון + ציטוט",
        "discussion_average": "ממוצע דיון",
    }
    for key, (lo, hi) in PARAGRAPH_LENGTHS.items():
        result += f"| {labels.get(key, key)} | {lo}-{hi} |\n"
    result += "\n"

    # Golden ratios
    result += "## יחסי הזהב (אחוזי סעיפים מסך ההחלטה)\n\n"
    result += "| סוג ערר | רקע | טענות | דיון | סיכום |\n"
    result += "|---------|------|-------|------|-------|\n"
    outcome_labels = {
        "rejection": "רישוי נדחה",
        "full_acceptance": "רישוי מתקבל",
        "partial_acceptance": "רישוי קבלה חלקית",
        "betterment_levy": "היטל השבחה",
    }
    for outcome in VALID_OUTCOMES:
        r = GOLDEN_RATIOS[outcome]
        result += (
            f"| {outcome_labels[outcome]} "
            f"| {r['background'][0]}-{r['background'][1]}% "
            f"| {r['claims'][0]}-{r['claims'][1]}% "
            f"| {r['discussion'][0]}-{r['discussion'][1]}% "
            f"| {r['summary'][0]}-{r['summary'][1]}% |\n"
        )
    # GAP-51: betterment_levy is a practice_area override (applied on top of the outcome), not an outcome.
    _bl = PRACTICE_AREA_OVERRIDES["betterment_levy"]["golden_ratios"]
    result += (
        f"| {outcome_labels['betterment_levy']} (override לפי תחום) "
        f"| {_bl['background'][0]}-{_bl['background'][1]}% "
        f"| {_bl['claims'][0]}-{_bl['claims'][1]}% "
        f"| {_bl['discussion'][0]}-{_bl['discussion'][1]}% "
        f"| {_bl['summary'][0]}-{_bl['summary'][1]}% |\n"
    )
    result += "\n"

    # T10 — measured-from-corpus ratios alongside the targets, ⚠️ flags a gap
    # (actual average outside the target range → revisit the target or the corpus).
    try:
        from legal_mcp.services.style_distance import measure_corpus_ratios
        measured = await measure_corpus_ratios()
        if measured:
            result += "### נמדד מהקורפוס בפועל (ממוצע) — ⚠️ = פער מהיעד\n\n"
            result += "| קבוצה | רקע | טענות | דיון | סיכום |\n|---|------|-------|------|-------|\n"
            # Per-outcome rows (flagged vs that outcome's target), when outcomes exist.
            for outcome in VALID_OUTCOMES:
                m = measured.get(outcome)
                if not m:
                    continue
                tgt = GOLDEN_RATIOS[outcome]
                cells = []
                for sec in ("background", "claims", "discussion", "summary"):
                    val = m["sections"].get(sec)
                    if val is None:
                        cells.append("—")
                        continue
                    lo, hi = tgt[sec]
                    cells.append(f"{val}%" + ("" if lo <= val <= hi else " ⚠️"))
                result += f"| {outcome_labels[outcome]} (n={m['n']}) | " + " | ".join(cells) + " |\n"
            # "_all" aggregate — the meaningful row today (corpus outcome unpopulated);
            # shown informationally (no single target to flag against).
            allm = measured.get("_all")
            if allm:
                cells = [f"{allm['sections'].get(s, '—')}%" if allm['sections'].get(s) is not None else "—"
                         for s in ("background", "claims", "discussion", "summary")]
                result += f"| כל ההחלטות (n={allm['n']}) | " + " | ".join(cells) + " |\n"
            result += ("\n_⚠️ = הממוצע בפועל חורג מטווח-היעד; שקול לעדכן יעד ב-/methodology או לבדוק את הקורפוס. "
                       "פיצול לפי-תוצאה יופיע כש-`style_corpus.outcome` יאוכלס._\n\n")
    except Exception as e:  # surfaced, not swallowed
        result += f"_מדידת יחסי-זהב מהקורפוס נכשלה: {e}_\n\n"

    # Opening and summary strategies
    result += "## אסטרטגיות פתיחה וסיכום לפי תוצאה\n\n"
    for outcome in VALID_OUTCOMES:
        opening = OPENING_STRATEGIES[outcome]
        summary = SUMMARY_STRATEGIES[outcome]
        result += f"### {outcome_labels[outcome]}\n"
        result += f"- **פתיחה:** {opening['description']} ({opening['paragraphs'][0]}-{opening['paragraphs'][1]} פסקאות)\n"
        result += f"- **סיכום ({summary['heading']}):** {summary['description']}\n\n"

    # GAP-51: betterment_levy override (practice_area, applied over the outcome)
    _bo = PRACTICE_AREA_OVERRIDES["betterment_levy"]
    _op, _sm = _bo["opening_strategy"], _bo["summary_strategy"]
    result += f"### {outcome_labels['betterment_levy']} (override לפי תחום)\n"
    result += f"- **פתיחה:** {_op['description']} ({_op['paragraphs'][0]}-{_op['paragraphs'][1]} פסקאות)\n"
    result += f"- **סיכום ({_sm['heading']}):** {_sm['description']}\n\n"

    return ok(result)


async def draft_section(
    case_number: str,
    section: str,
    instructions: str = "",
) -> str:
    """DEPRECATED (GAP-50/INV-TOOL2): העדף את get_block_context — הקשר לפי-בלוק,
    התואם לארכיטקטורת 12-הבלוקים הקנונית. כלי זה מרכיב הקשר לפי "סעיף"
    (granularity ישן וחופף ל-get_block_context) ונשמר זמנית לתאימות-לאחור.

    הרכבת הקשר מלא לניסוח סעיף בהחלטה - כולל עובדות מהמסמכים, תקדימים רלוונטיים ודפוסי סגנון.

    Args:
        case_number: מספר תיק הערר
        section: סוג הסעיף (facts, appellant_claims, respondent_claims, legal_analysis, conclusion, ruling)
        instructions: הנחיות נוספות לניסוח

    כל קטע ב-case_documents/precedents מלווה ב-provenance: document_id, page
    (מספר עמוד במסמך-המקור, אם קיים) ו-score — כדי שניתן יהיה לעקוב אחורה
    אל המקור ולצטטו, ולא לסמוך על התוכן ללא מקור.
    """
    case = await db.get_case_by_number(case_number)
    if not case:
        return err(f"תיק {case_number} לא נמצא.")

    case_id = UUID(case["id"])
    expected_outcome = case.get("expected_outcome", "")

    # 1. Get relevant chunks from case documents
    section_query = {
        "facts": "רקע עובדתי של התיק",
        "appellant_claims": "טענות העוררים",
        "respondent_claims": "טענות המשיבים",
        "legal_analysis": "ניתוח משפטי ודיון",
        "conclusion": "מסקנות",
        "ruling": "החלטה",
    }.get(section, section)

    query_emb = await embeddings.embed_query(section_query)
    case_chunks = await db.search_similar(
        query_embedding=query_emb, limit=10, case_id=case_id
    )

    # 2. Get similar sections from precedents
    precedent_chunks = await db.search_similar(
        query_embedding=query_emb, limit=5, section_type=section
    )
    # Filter out chunks from the same case
    precedent_chunks = [c for c in precedent_chunks if str(c["case_id"]) != case["id"]]

    # 3. Get style patterns
    style_patterns = await db.get_style_patterns()

    # Build context
    context = {
        "case": {
            "case_number": case["case_number"],
            "title": case["title"],
            "appellants": case["appellants"],
            "respondents": case["respondents"],
            "subject": case["subject"],
            "property_address": case["property_address"],
            "expected_outcome": expected_outcome,
        },
        "section": section,
        "instructions": instructions,
        # GAP-47 (INV-TOOL4/G9): surface provenance — document_id + page —
        # so the writer can cite chunks back to their source (already fetched
        # by search_similar, was previously discarded).
        "case_documents": [
            {
                "document": c["document_title"],
                "document_id": str(c["document_id"]),
                "page": c.get("page_number"),
                "section_type": c["section_type"],
                "score": round(c.get("score", 0.0), 4),
                "content": c["content"],
            }
            for c in case_chunks
        ],
        "precedents": [
            {
                "case_number": c["case_number"],
                "document": c["document_title"],
                "document_id": str(c["document_id"]),
                "page": c.get("page_number"),
                "score": round(c.get("score", 0.0), 4),
                "content": c["content"][:500],
            }
            for c in precedent_chunks[:3]
        ],
        "style_patterns": [
            {
                "type": p["pattern_type"],
                "text": p["pattern_text"],
            }
            for p in style_patterns[:15]
        ],
    }

    # 4. Add outcome-aware drafting guidance
    if expected_outcome and expected_outcome in VALID_OUTCOMES:
        lessons = get_lessons_for_outcome(expected_outcome)
        guidance: dict = {
            "outcome": expected_outcome,
            "golden_ratios": lessons["golden_ratios"],
            "citation_guidance": lessons["citation_guidance"],
        }

        # Section-specific guidance
        if section == "legal_analysis":
            guidance["discussion_rules"] = lessons["discussion_rules"]
            guidance["opening_strategy"] = lessons["opening_strategy"]
            guidance["transition_phrases"] = lessons["transition_phrases"]
            guidance["paragraph_lengths"] = lessons["paragraph_lengths"]
        elif section in ("conclusion", "ruling"):
            guidance["summary_strategy"] = lessons["summary_strategy"]
        elif section == "facts":
            guidance["target_ratio"] = lessons["golden_ratios"].get("background", "")
            guidance["paragraph_lengths"] = {"claims": lessons["paragraph_lengths"].get("claims", "")}
        elif section in ("appellant_claims", "respondent_claims"):
            guidance["target_ratio"] = lessons["golden_ratios"].get("claims", "")
            guidance["paragraph_lengths"] = {"claims": lessons["paragraph_lengths"].get("claims", "")}

        context["drafting_guidance"] = guidance

    return ok(context)


async def get_chair_directions(case_number: str) -> str:
    """שליפת עמדות יו"ר הוועדה (דפנה) על סוגיות הערר, לצורך יצירת direction_doc
    לכותב. קורא מ-analysis-and-research.md (שנוצר ע"י legal-analyst ומולא ע"י
    דפנה דרך ה-UI).

    מחזיר JSON עם סטטוס, כמה סוגיות מולאו וכמה עדיין ריקות, ורשימה של עמדות
    מובנות — ניתן להזריק ישירות כ-direction_doc לבלוק י (דיון) ולבלוק יא (סיכום).

    סטטוסים:
      missing  — הקובץ לא קיים
      empty    — הקובץ קיים אבל כל העמדות ריקות (טרם נקבעה דעה)
      partial  — חלק מהעמדות מולאו
      complete — כל העמדות מולאו

    אם המצב הוא `empty` או `missing` — הכותב צריך לעצור ולבקש מדפנה למלא
    את הקובץ דרך ה-UI לפני המשך הכתיבה.

    Args:
        case_number: מספר תיק הערר
    """
    case_dir = config.find_case_dir(case_number)
    file_path = case_dir / "documents" / "research" / "analysis-and-research.md"
    result = research_md.extract_chair_directions(file_path)
    return ok(result)


async def get_decision_template(case_number: str) -> str:
    """קבלת תבנית מבנית להחלטה מלאה עם פרטי התיק, מותאמת לסוג התוצאה הצפויה.

    Args:
        case_number: מספר תיק הערר
    """
    from datetime import date

    case = await db.get_case_by_number(case_number)
    if not case:
        return err(f"תיק {case_number} לא נמצא.")

    # GAP-51: canonicalize outcome + apply betterment_levy practice_area override.
    expected_outcome = canonical_outcome(case.get("expected_outcome", ""))
    practice_area = case.get("practice_area", "")

    format_args = dict(
        case_number=case["case_number"],
        subject=case["subject"],
        appellants=", ".join(case.get("appellants", [])),
        respondents=", ".join(case.get("respondents", [])),
        property_address=case.get("property_address", ""),
        date=date.today().strftime("%d.%m.%Y"),
    )

    # Use outcome-specific template if available
    if expected_outcome and expected_outcome in DECISION_TEMPLATES:
        # Add ratio comments (practice_area-aware)
        format_args["ratios_background"] = format_ratios_comment(expected_outcome, "background", practice_area)
        format_args["ratios_claims"] = format_ratios_comment(expected_outcome, "claims", practice_area)
        format_args["ratios_discussion"] = format_ratios_comment(expected_outcome, "discussion", practice_area)
        format_args["ratios_summary"] = format_ratios_comment(expected_outcome, "summary", practice_area)

        # betterment_levy practice_area supplies its own template; else use the outcome's.
        override = PRACTICE_AREA_OVERRIDES.get(practice_area, {})
        template_src = override.get("decision_template") or DECISION_TEMPLATES[expected_outcome]
        template = template_src.format(**format_args)

        # Add guidance header (override-aware via get_lessons_for_outcome)
        lessons_o = get_lessons_for_outcome(expected_outcome, practice_area)
        opening = lessons_o["opening_strategy"]
        summary = lessons_o["summary_strategy"]
        header = (
            f"<!-- תבנית מותאמת ל: {expected_outcome}"
            f"{' / ' + practice_area if practice_area in PRACTICE_AREA_OVERRIDES else ''} -->\n"
            f"<!-- פתיחת דיון: {opening['description']} -->\n"
            f"<!-- סיכום: {summary['description']} -->\n\n"
        )
        return ok(header + template)
    else:
        # Fallback to generic template
        template = DECISION_TEMPLATE.format(**format_args)
        if not expected_outcome:
            template = (
                "<!-- לא הוגדרה תוצאה צפויה. הגדר expected_outcome בתיק לקבלת תבנית מותאמת. -->\n"
                f"<!-- ערכים אפשריים: {', '.join(VALID_OUTCOMES)} -->\n\n"
            ) + template
        return ok(template)


async def validate_decision(case_number: str) -> str:
    """בדיקת QA אוטומטית על ההחלטה — 6 בדיקות. אם נכשלת בדיקה קריטית — ייצוא חסום.

    Args:
        case_number: מספר תיק הערר
    """
    from legal_mcp.services import qa_validator

    case = await db.get_case_by_number(case_number)
    if not case:
        return err(f"תיק {case_number} לא נמצא.")

    case_id = UUID(case["id"])

    try:
        result = await qa_validator.validate_decision(case_id)
        return ok(result)
    except ValueError as e:
        return err(str(e))


async def export_docx(case_number: str, output_path: str = "") -> str:
    """ייצוא החלטה לקובץ DOCX מעוצב — גופן David, RTL, כותרות, מספור סעיפים.

    הקובץ נוצר עם bookmarks ב-12 הבלוקים (אנקורים ל-revisions עתידיים),
    ומסומן כ-active_draft_path של התיק.

    Args:
        case_number: מספר תיק הערר
        output_path: נתיב לשמירה (אופציונלי — ברירת מחדל: תיקיית התיק)
    """
    from legal_mcp.services import docx_exporter

    case = await db.get_case_by_number(case_number)
    if not case:
        return err(f"תיק {case_number} לא נמצא.")

    case_id = UUID(case["id"])

    # INV-EX3 / INV-QA3: a decision cannot be exported while critical QA gates
    # fail (or before QA has been run at all). Gate on the STORED qa_results —
    # cheap SELECT, no LLM re-run.
    if not await db.qa_run_exists(case_id):
        return err("ייצוא נחסם: בקרת איכות (QA) טרם רצה על התיק. "
                   "הרץ validate_decision לפני ייצוא.")

    critical = await db.get_critical_qa_failures(case_id)
    if critical:
        gate_names = ", ".join(r["check_name"] for r in critical)
        return err(
            f"ייצוא נחסם: שערי QA קריטיים נכשלו ({gate_names}). "
            f"תקן את הליקויים והרץ validate_decision מחדש לפני ייצוא.",
            data={"failed_gates": [r["check_name"] for r in critical]},
        )

    try:
        path = await docx_exporter.export_decision(case_id, output_path or None)
        # Register this export as the new source of truth
        await db.set_active_draft_path(case_id, path)
        await audit.log_action_safe(
            "export_docx", case_id=case_id,
            details={"path": str(path)},
        )
        await db.mark_blocks_stale(case_id, False)
        case_dir = config.find_case_dir(case_number)
        if case_dir.exists():
            git_sync.commit_and_push(case_dir, f"ייצוא DOCX: {Path(path).name}")
        return ok({
            "path": path,
            "active_draft_path": path,
            "message": f"DOCX נוצר: {path}",
        })
    except ValueError as e:
        return err(str(e))


# ── Interim draft (pre-ruling) ────────────────────────────────────

# Blocks written for the interim draft, in a FIXED order.
# This is the same content the chair sees in the final decision (same template,
# same skill, same prompts, same single canonical write path — block_writer) —
# minus ruling, summary, signatures.
#
# Determinism (#204 / WS5): the interim draft is STRUCTURALLY deterministic.
# Each block is generated by block_writer.write_block, which now pins the model
# (Opus 4.8) and a per-block reasoning effort, and uses fixed structural prompts
# (e.g. block-he's opening is always "לפנינו ערר…"). The display order on export
# is fixed separately by docx_exporter._INTERIM_BLOCK_ORDER. There is no parallel
# generation path (G2): write_interim_draft is a thin orchestrator over the same
# write_and_store_block used by the full decision.
_INTERIM_BLOCKS = ["block-he", "block-vav", "block-tet", "block-zayin", "block-chet"]


async def extract_appraiser_facts(case_number: str) -> str:
    """חילוץ תכניות והיתרים מכל השומות בתיק וזיהוי סתירות בין שמאים.

    משמש כהכנה לטיוטת ביניים: בלוק ט (תכניות חלות) זקוק לעובדות מובנות
    כדי לפרט תת-פרק היתרים ולסמן סתירות בנוסח ניטרלי.

    Args:
        case_number: מספר תיק הערר
    """
    from legal_mcp.services import appraiser_facts_extractor

    case = await db.get_case_by_number(case_number)
    if not case:
        return err(f"תיק {case_number} לא נמצא.")
    case_id = UUID(case["id"])
    try:
        result = await appraiser_facts_extractor.extract_appraiser_facts(case_id)
        return ok(result)
    except Exception as e:
        return err(str(e))


async def get_appraiser_facts(case_number: str) -> str:
    """קריאת עובדות-השמאי שכבר חולצו לתיק — ללא הרצת חילוץ מחדש (INV-TOOL4 / GAP-44).

    ה-get המקביל ל-extract_appraiser_facts: מחזיר את העובדות השמורות בטבלת
    appraiser_facts + סתירות מזוהות בין שמאים, בלי קריאת-LLM יקרה ולא-דטרמיניסטית.
    מחזיר facts ריק אם החילוץ טרם רץ (status=ok, count=0) — לא שגיאה.

    Args:
        case_number: מספר תיק הערר
    """
    case = await db.get_case_by_number(case_number)
    if not case:
        return err(f"תיק {case_number} לא נמצא.")
    case_id = UUID(case["id"])
    try:
        facts = await db.list_appraiser_facts(case_id)
        conflicts = await db.detect_appraiser_conflicts(case_id)
        return ok({
            "case_number": case_number,
            "count": len(facts),
            "facts": facts,
            "conflicts": conflicts,
        })
    except Exception as e:
        return err(str(e))


async def analyze_protocol(case_number: str, document_id: str = "") -> str:
    """ניתוח השוואתי של פרוטוקול-דיון מול כתבי-הטענות (WS4 / #203).

    מזהה אילו טענות **ירדו** (נזנחו בדיון), אילו **חוזקו**, ואילו **עלו חדשות**,
    מחדד את השאלות המשפטיות, ומחלץ את נתוני הכותרת (א–ד) מהפרוטוקול. התוצאה נכנסת
    ל"ידע-התיק" (טבלת protocol_analysis), זמינה לסוכני הניתוח והכתיבה; תאריך-הדיון
    מוזן חזרה לעמודה הקנונית cases.hearing_date.

    בוחר אוטומטית את פרוטוקול **ועדת-הערר** (protocol_scope שאינו 'lower');
    פרוטוקול ועדה-מקומית ('lower') אינו נכנס להשוואה. כשיש כמה פרוטוקולי-ערר —
    ציין document_id כדי לכוון לדיון המדויק (#223).

    דורש פרוטוקול מתויג doc_type='protocol' + טיעונים מאוגדים
    (aggregate_claims_to_arguments). רץ עם Claude מקומי (Opus 4.8, effort=high);
    re-run מחליף את הניתוח הקודם לאותו פרוטוקול (idempotent).

    Args:
        case_number: מספר תיק הערר
        document_id: מזהה מסמך-הפרוטוקול לכיוון מדויק (ריק = בחירה אוטומטית)
    """
    from legal_mcp.services import protocol_analyzer

    case = await db.get_case_by_number(case_number)
    if not case:
        return err(f"תיק {case_number} לא נמצא.")
    case_id = UUID(case["id"])
    target_doc_id: UUID | None = None
    if document_id.strip():
        try:
            target_doc_id = UUID(document_id.strip())
        except ValueError:
            return err(f"document_id לא תקין: {document_id}")
    try:
        result = await protocol_analyzer.analyze_protocol(
            case_id, target_document_id=target_doc_id,
        )
        await audit.log_action_safe(
            "analyze_protocol", case_id=case_id,
            details={"status": result.get("status"), "total": result.get("total", 0)},
        )
        return ok(result)
    except Exception as e:
        return err(str(e))


async def get_protocol_analysis(case_number: str, change_type: str = "") -> str:
    """קריאת ניתוח-הפרוטוקול שכבר חולץ — ללא הרצת ניתוח-מחדש (INV-TOOL4).

    ה-get המקביל ל-analyze_protocol: מחזיר את רשומות ידע-התיק השמורות
    (ירדה/חוזקה/חדשה + שאלה-מחודדת + ציטוט-מבסס), בלי קריאת-LLM יקרה.
    מחזיר רשימה ריקה אם הניתוח טרם רץ (status=ok, count=0) — לא שגיאה.

    Args:
        case_number: מספר תיק הערר
        change_type: סינון (dropped/strengthened/newly_raised). ריק = הכל.
    """
    from legal_mcp.services import protocol_analyzer

    case = await db.get_case_by_number(case_number)
    if not case:
        return err(f"תיק {case_number} לא נמצא.")
    if change_type and change_type not in protocol_analyzer.VALID_CHANGE_TYPES:
        return err(
            f"change_type לא תקין: {change_type}",
            data={"allowed": sorted(protocol_analyzer.VALID_CHANGE_TYPES)},
        )
    case_id = UUID(case["id"])
    try:
        rows = await db.list_protocol_analysis(
            case_id, change_type=change_type or None,
        )
        return ok({
            "case_number": case_number,
            "count": len(rows),
            "analysis": rows,
        })
    except Exception as e:
        return err(str(e))


async def write_interim_draft(case_number: str, instructions: str = "") -> str:
    """כתיבת ארבעת הבלוקים לטיוטת ביניים: רקע (ו), תכניות+היתרים (ט),
    טענות הצדדים (ז), הליכים (ח). אם לא חולצו עובדות שמאיות עדיין —
    מריץ extract_appraiser_facts קודם כדי שבלוק ט יקבל פרק היתרים תקף.

    הבלוקים נכתבים באותו skill, אותם prompts ואותו טמפלט כמו בטיוטה רגילה —
    הסדר משתנה רק בעת הייצוא ל-DOCX (ראה export_interim_draft).

    Args:
        case_number: מספר תיק הערר
        instructions: הנחיות נוספות (לכל הבלוקים)
    """
    from legal_mcp.services import appraiser_facts_extractor, block_writer

    case = await db.get_case_by_number(case_number)
    if not case:
        return err(f"תיק {case_number} לא נמצא.")
    case_id = UUID(case["id"])

    # Make sure appraiser facts exist before writing block-tet (which depends on them).
    facts = await db.list_appraiser_facts(case_id)
    facts_run: dict | None = None
    if not facts:
        try:
            facts_run = await appraiser_facts_extractor.extract_appraiser_facts(case_id)
        except Exception as e:
            facts_run = {"status": "error", "message": str(e)}

    results = []
    for bid in _INTERIM_BLOCKS:
        try:
            r = await block_writer.write_and_store_block(case_id, bid, instructions)
            results.append({
                "block_id": bid,
                "title": r["title"],
                "word_count": r["word_count"],
                "status": "completed",
            })
        except Exception as e:
            results.append({
                "block_id": bid,
                "status": "error",
                "error": str(e),
            })

    return ok({
        "blocks": results,
        "appraiser_facts_run": facts_run,
        "total_words": sum(r.get("word_count", 0) for r in results),
        "completed": sum(1 for r in results if r["status"] == "completed"),
    })


async def export_interim_draft(case_number: str, output_path: str = "") -> str:
    """ייצוא טיוטת ביניים ל-DOCX — אותו עיצוב של טיוטה רגילה (David, RTL,
    bookmarks), אבל בסדר חדש: רקע → תכניות+היתרים → טענות → הליכים, ללא
    דיון/סיכום/חתימות. שם הקובץ: טיוטה-טענות_הצדדים_{N}.docx.

    Args:
        case_number: מספר תיק הערר
        output_path: נתיב לשמירה (אופציונלי)
    """
    from legal_mcp.services import docx_exporter

    case = await db.get_case_by_number(case_number)
    if not case:
        return err(f"תיק {case_number} לא נמצא.")
    case_id = UUID(case["id"])

    try:
        path = await docx_exporter.export_decision(
            case_id, output_path or None, mode="interim",
        )
        await db.set_active_draft_path(case_id, path)
        case_dir = config.find_case_dir(case_number)
        if case_dir.exists():
            git_sync.commit_and_push(case_dir, f"טיוטת ביניים: {Path(path).name}")
        return ok({
            "mode": "interim",
            "path": path,
            "active_draft_path": path,
            "message": f"טיוטת ביניים נוצרה: {path}",
        })
    except ValueError as e:
        return err(str(e))


async def summarize_party_claims(case_number: str, instructions: str = "") -> str:
    """הפקת סיכום-מנהלים מזוקק של טענות הצדדים — מסמך פרוזה **נפרד מטיוטת-הביניים**,
    להכנת היו"ר לדיון בעל-פה. תמצית קצרה ומוקפדת (לא שכפול כתבי-הטענות), נגזרת
    מהטיעונים המאוגדים (legal_arguments) או מ-claims הגולמיים — אותו מקור-אמת,
    בלי חילוץ-מחדש. נשמר ל-data/cases/{n}/documents/research/ (git + S3) וניתן-לייצוא.

    הייצור עובר claude_session (claude -p), נעוץ Opus 4.8 + effort=high. מתוחם
    לטענות-התיק בלבד (anti-hallucination) — אינו ממציא טענה/הלכה/עובדה שאינה בקלט.

    Args:
        case_number: מספר תיק הערר
        instructions: הנחיות נוספות מהיו"ר (אופציונלי)
    """
    from legal_mcp.services import party_claims_summary

    case = await db.get_case_by_number(case_number)
    if not case:
        return err(f"תיק {case_number} לא נמצא.")
    case_id = UUID(case["id"])
    try:
        result = await party_claims_summary.summarize_party_claims(
            case_id, case_number, instructions,
        )
        return ok(result)
    except ValueError as e:
        return err(str(e))


async def apply_user_edit(case_number: str, edit_filename: str) -> str:
    """רישום עריכה שהעלה המשתמש כמקור האמת החדש של התיק.

    התהליך:
    1. מאתר את הקובץ `עריכה-v*.docx` בתיקיית ה-exports
    2. מזריק bookmarks רטרואקטיבית (אם אין) דרך docx_retrofit
    3. מעדכן את cases.active_draft_path

    Args:
        case_number: מספר תיק הערר
        edit_filename: שם הקובץ (למשל "עריכה-v1.docx") או נתיב מלא
    """
    from legal_mcp.services import docx_retrofit

    case = await db.get_case_by_number(case_number)
    if not case:
        return err(f"תיק {case_number} לא נמצא.")

    case_id = UUID(case["id"])
    export_dir = config.find_case_dir(case_number) / "exports"
    edit_path = export_dir / edit_filename if "/" not in edit_filename else Path(edit_filename)
    if not edit_path.exists():
        return err(f"קובץ לא נמצא: {edit_path}")

    try:
        retrofit_result = docx_retrofit.retrofit_bookmarks(edit_path)
        await db.set_active_draft_path(case_id, str(edit_path))
        await db.mark_blocks_stale(case_id, True)
        case_dir = config.find_case_dir(case_number)
        if case_dir.exists():
            git_sync.commit_and_push(case_dir, f"גרסת עריכה: {edit_path.name}")
        return ok({
            "active_draft_path": str(edit_path),
            "bookmarks_added": retrofit_result.get("bookmarks_added", []),
            "missing_blocks": retrofit_result.get("missing_blocks", []),
            "structural_fallback": retrofit_result.get("structural_fallback", []),
            "existing_bookmarks": retrofit_result.get("existing_bookmarks", []),
        })
    except Exception as e:
        return err(str(e))


async def list_bookmarks(case_number: str) -> str:
    """רשימת bookmarks הקיימים ב-active_draft של התיק.

    משמש לסוכנים כדי לדעת אילו אנקורים זמינים לפני שליחת revisions.
    """
    from legal_mcp.services import docx_reviser

    case = await db.get_case_by_number(case_number)
    if not case:
        return err(f"תיק {case_number} לא נמצא.")

    active_path = await db.get_active_draft_path(UUID(case["id"]))
    if not active_path or not Path(active_path).exists():
        return empty("לא נמצא active_draft. הרץ ייצוא או העלה עריכה.")

    try:
        names = docx_reviser.list_bookmarks(active_path)
        return ok({
            "active_draft_path": active_path,
            "bookmarks": names,
        })
    except Exception as e:
        return err(str(e))


async def revise_draft(case_number: str, revisions_json: str,
                       author: str = "מערכת AI") -> str:
    """החלת revisions מסומנים כ-Track Changes על ה-active_draft של התיק.

    יוצר קובץ חדש `טיוטה-v{N+1}.docx` (מגרסה הבאה בתור), ומעדכן את
    active_draft_path אליו.

    Args:
        case_number: מספר תיק הערר
        revisions_json: JSON string של array עם אובייקטים:
            [{"id": "r1", "type": "insert_after"|"insert_before"|"replace"|"delete",
              "anchor_bookmark": "block-yod", "content": "...", "style": "body"|"heading"|"quote",
              "reason": "..."}, ...]
        author: מחרוזת המחבר שתופיע ב-Track Changes
    """
    from legal_mcp.services import docx_reviser

    case = await db.get_case_by_number(case_number)
    if not case:
        return err(f"תיק {case_number} לא נמצא.")

    case_id = UUID(case["id"])
    active_path = await db.get_active_draft_path(case_id)
    if not active_path or not Path(active_path).exists():
        return err("אין active_draft. הרץ ייצוא או apply_user_edit קודם.")

    try:
        raw = json.loads(revisions_json) if isinstance(revisions_json, str) else revisions_json
    except json.JSONDecodeError as e:
        return err(f"JSON לא תקף: {e}")

    revisions = []
    for item in raw:
        revisions.append(docx_reviser.Revision(
            id=item.get("id", ""),
            type=item["type"],
            anchor_bookmark=item["anchor_bookmark"],
            content=item.get("content", ""),
            style=item.get("style", "body"),
            reason=item.get("reason", ""),
            anchor_position=item.get("anchor_position", "end"),
        ))

    # Determine output path — next טיוטה-v{N}.docx
    export_dir = config.find_case_dir(case_number) / "exports"
    export_dir.mkdir(parents=True, exist_ok=True)
    existing = list(export_dir.glob("טיוטה-v*.docx"))
    next_ver = 1
    for p in existing:
        try:
            ver = int(p.stem.split("-v")[1])
            next_ver = max(next_ver, ver + 1)
        except (IndexError, ValueError):
            pass
    output_path = export_dir / f"טיוטה-v{next_ver}.docx"

    try:
        result = docx_reviser.apply_tracked_revisions(
            active_path, output_path, revisions, author=author,
        )
        await db.set_active_draft_path(case_id, str(output_path))
        await db.mark_blocks_stale(case_id, True)
        case_dir = config.find_case_dir(case_number)
        if case_dir.exists():
            git_sync.commit_and_push(
                case_dir,
                f"revise: טיוטה-v{next_ver} ({result.applied} שינויים, {result.failed} נכשלו)",
            )
        return ok({
            "output_path": str(output_path),
            "version": next_ver,
            "applied": result.applied,
            "failed": result.failed,
            "active_draft_path": str(output_path),
            "results": [
                {"id": r.id, "status": r.status, "error": r.error}
                for r in result.results
            ],
        })
    except Exception as e:
        return err(str(e))


async def get_block_context(case_number: str, block_id: str, instructions: str = "") -> str:
    """קבלת הקשר מלא לכתיבת בלוק — ללא קריאה ל-API. Claude Code כותב את הבלוק.

    Args:
        case_number: מספר תיק הערר
        block_id: מזהה הבלוק (block-he, block-vav, ..., block-yod-bet)
        instructions: הנחיות נוספות
    """
    from legal_mcp.services import block_writer

    case = await db.get_case_by_number(case_number)
    if not case:
        return err(f"תיק {case_number} לא נמצא.")

    case_id = UUID(case["id"])
    try:
        ctx = await block_writer.get_block_context(case_id, block_id, instructions)
        return ok(ctx)
    except ValueError as e:
        return err(str(e))


async def save_block_content(case_number: str, block_id: str, content: str) -> str:
    """שמירת בלוק שנכתב ע"י Claude Code ב-DB.

    Args:
        case_number: מספר תיק הערר
        block_id: מזהה הבלוק
        content: הטקסט שנכתב
    """
    from legal_mcp.services import block_writer

    case = await db.get_case_by_number(case_number)
    if not case:
        return err(f"תיק {case_number} לא נמצא.")

    case_id = UUID(case["id"])
    try:
        result = await block_writer.save_block_content(case_id, block_id, content)
        return ok(result)
    except ValueError as e:
        return err(str(e))


async def analyze_style(appeal_subtype: str = "") -> str:
    """הרצת ניתוח סגנון על קורפוס ההחלטות של דפנה. מחלץ דפוסי כתיבה ושומר אותם.

    Args:
        appeal_subtype: סינון לפי סוג ערר (building_permit / betterment_levy / compensation_197).
                        ריק = כל ההחלטות.
    """
    from legal_mcp.services.style_analyzer import analyze_corpus

    result = await analyze_corpus(appeal_subtype)
    return ok(result)


async def write_block(
    case_number: str,
    block_id: str,
    instructions: str = "",
) -> str:
    """כתיבת בלוק יחיד בהחלטה. כותב ושומר ב-DB.

    Args:
        case_number: מספר תיק הערר
        block_id: מזהה הבלוק: block-alef, block-bet, block-gimel, block-dalet, block-he, block-vav, block-zayin, block-chet, block-tet, block-yod, block-yod-alef, block-yod-bet
        instructions: הנחיות נוספות לניסוח
    """
    from legal_mcp.services import block_writer

    case = await db.get_case_by_number(case_number)
    if not case:
        return err(f"תיק {case_number} לא נמצא.")

    case_id = UUID(case["id"])

    try:
        result = await block_writer.write_and_store_block(case_id, block_id, instructions)
        return ok(result)
    except ValueError as e:
        return err(str(e))


async def write_all_blocks(
    case_number: str,
    start_from: str = "block-alef",
    instructions: str = "",
) -> str:
    """כתיבת כל הבלוקים בהחלטה, בלוק אחרי בלוק. שומר כל בלוק מיד אחרי כתיבה.

    Args:
        case_number: מספר תיק הערר
        start_from: מאיזה בלוק להתחיל (ברירת מחדל: block-alef)
        instructions: הנחיות כלליות
    """
    from legal_mcp.services import block_writer
    from legal_mcp.services.block_writer import BLOCK_CONFIG

    case = await db.get_case_by_number(case_number)
    if not case:
        return err(f"תיק {case_number} לא נמצא.")

    case_id = UUID(case["id"])

    # Determine start index
    start_idx = BLOCK_CONFIG.get(start_from, {}).get("index", 1)

    results = []
    block_order = sorted(BLOCK_CONFIG.items(), key=lambda x: x[1]["index"])

    for bid, cfg in block_order:
        if cfg["index"] < start_idx:
            continue

        try:
            result = await block_writer.write_and_store_block(case_id, bid, instructions)
            results.append({
                "block_id": bid,
                "title": result["title"],
                "word_count": result["word_count"],
                "status": "completed",
            })
        except ValueError as e:
            results.append({
                "block_id": bid,
                "title": cfg["title"],
                "status": "error",
                "error": str(e),
            })
            # Stop on critical error (e.g., missing direction for block-yod)
            if "כיוון מאושר" in str(e):
                break

    total_words = sum(r.get("word_count", 0) for r in results)
    return ok({
        "blocks": results,
        "total_words": total_words,
        "completed": sum(1 for r in results if r["status"] == "completed"),
    })
