"""Ezer Mishpati - MCP Server entry point.

Run with: python -m legal_mcp.server
"""

from __future__ import annotations

import logging
import os
import sys
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager

from mcp.server.fastmcp import FastMCP

# Configure logging to stderr (stdout is reserved for JSON-RPC)
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
    stream=sys.stderr,
)
logger = logging.getLogger("legal_mcp")


@asynccontextmanager
async def lifespan(server: FastMCP) -> AsyncIterator[None]:
    """Server startup is now non-blocking.

    Schema init was moved out of the lifespan to fix a race where Claude Code
    would call a tool before `tools/list` had been answered — manifesting as
    "No such tool available". Lifespan now returns immediately so the MCP
    handshake completes in milliseconds; the schema is initialized lazily on
    the first DB access via services/db.get_pool().
    """
    from legal_mcp.services.db import close_pool

    logger.info("Ezer Mishpati MCP server ready (schema init deferred)")
    try:
        yield
    finally:
        await close_pool()
        logger.info("Ezer Mishpati MCP server stopped")


# HTTP listener address, used only when MCP_TRANSPORT selects an HTTP transport.
#
# These MUST be passed to the constructor rather than left to FastMCP's own
# FASTMCP_HOST / FASTMCP_PORT environment settings: FastMCP.__init__ declares
# `host: str = "127.0.0.1"` and `port: int = 8000` as explicit keyword defaults
# and forwards them into Settings(**settings). In pydantic-settings, explicit
# init kwargs outrank environment variables — so FASTMCP_PORT is silently
# ignored and the server binds 8000 regardless (verified 2026-08-05; on this
# host 8000 is already taken, so it failed loudly by luck rather than design).
#
# Default to loopback. The bearer gate below is the real protection; the narrow
# bind is defence in depth, not a substitute for it.
MCP_HTTP_HOST = os.environ.get("MCP_HTTP_HOST", "127.0.0.1")
MCP_HTTP_PORT = int(os.environ.get("MCP_HTTP_PORT", "8790"))

# Bearer gate — wired only when an HTTP transport is actually selected (#231.2).
#
# stdio must never require a token: the pipe is the boundary there, and every
# interactive session reaches us that way. Demanding a token on stdio would
# break all of them for no security gain.
#
# Building the verifier at import time (rather than inside main()) is deliberate:
# FastMCP takes `token_verifier` and `auth` as constructor arguments, so a
# missing token has to fail here — before the listener exists — not after it is
# already accepting connections.
_http_transport = os.environ.get("MCP_TRANSPORT", "stdio").strip() in ("sse", "streamable-http")
_auth_kwargs: dict = {}
if _http_transport:
    from mcp.server.auth.settings import AuthSettings

    from legal_mcp.services.http_auth import StaticTokenVerifier, load_token

    _base_url = f"http://{MCP_HTTP_HOST}:{MCP_HTTP_PORT}"
    _auth_kwargs = {
        "token_verifier": StaticTokenVerifier(load_token()),
        # AuthSettings is what switches on the SDK's BearerAuthBackend. We are a
        # resource server with a pre-shared token, not an OAuth client, so both
        # URLs simply point at ourselves — they exist to satisfy the protected-
        # resource metadata contract, and nothing issues tokens from them.
        "auth": AuthSettings(issuer_url=_base_url, resource_server_url=_base_url),
    }

# Create MCP server
mcp = FastMCP(
    "Ezer Mishpati - עוזר משפטי",
    instructions="מערכת AI לסיוע בניסוח החלטות משפטיות בסגנון דפנה תמיר",
    lifespan=lifespan,
    host=MCP_HTTP_HOST,
    port=MCP_HTTP_PORT,
    **_auth_kwargs,
)

# ── Import and register tools ───────────────────────────────────────

from legal_mcp.tools import (  # noqa: E402
    cases, documents, search, drafting, workflow, precedents,
    precedent_library as plib,
    internal_decisions as int_tools,
    legal_arguments as la_tools,
    missing_precedents as mp_tools,
    citations as cit_tools,
    training_enrichment as train_tools,
    digests as digest_tools,
    court_fetch as cf_tools,
    plans as plans_tools,
)


# Case management
@mcp.tool()
async def case_create(
    case_number: str,
    title: str,
    appellants: list[str] | None = None,
    respondents: list[str] | None = None,
    subject: str = "",
    property_address: str = "",
    permit_number: str = "",
    committee_type: str = "ועדה מקומית",
    hearing_date: str = "",
    notes: str = "",
    expected_outcome: str = "",
) -> str:
    """יצירת תיק ערר חדש. expected_outcome: rejection/partial_acceptance/full_acceptance/betterment_levy."""
    return await cases.case_create(
        case_number, title, appellants, respondents,
        subject, property_address, permit_number, committee_type,
        hearing_date, notes, expected_outcome,
    )


# INV-TOOL5 / GAP-53: hard cap on list/search result sizes (OWASP API4:2023 —
# Unrestricted Resource Consumption). Non-positive is treated as "max", not "all".
_MAX_LIMIT = 200


def _clamp_limit(limit: int, hard_max: int = _MAX_LIMIT) -> int:
    """Clamp a caller-supplied result limit to [1, hard_max]."""
    try:
        n = int(limit)
    except (TypeError, ValueError):
        return hard_max
    return hard_max if n <= 0 else min(n, hard_max)


@mcp.tool()
async def case_list(status: str = "", limit: int = 50) -> str:
    """רשימת תיקי ערר. סינון אופציונלי לפי סטטוס (new/processing/documents_ready/outcome_set/direction_approved/qa_review/drafted/exported/reviewed/final)."""
    return await cases.case_list(status, _clamp_limit(limit))


@mcp.tool()
async def case_get(case_number: str) -> str:
    """פרטי תיק מלאים כולל רשימת מסמכים."""
    return await cases.case_get(case_number)


@mcp.tool()
async def case_update(
    case_number: str,
    status: str = "",
    title: str = "",
    subject: str = "",
    notes: str = "",
    hearing_date: str = "",
    decision_date: str = "",
    tags: list[str] | None = None,
    expected_outcome: str = "",
) -> str:
    """עדכון פרטי תיק. expected_outcome: rejection/partial_acceptance/full_acceptance (betterment_levy הוא practice_area, לא תוצאה)."""
    return await cases.case_update(
        case_number, status, title, subject, notes,
        hearing_date, decision_date, tags, expected_outcome,
    )


@mcp.tool()
async def case_delete(case_number: str, remove_files: bool = False) -> str:
    """מחיקת תיק ערר. קבצים בדיסק נשארים אלא אם remove_files=true."""
    return await cases.case_delete(case_number, remove_files)


@mcp.tool()
async def case_get_final_text(case_number: str, max_chars: int = 0) -> str:
    """קליטת טקסט ההחלטה הסופית (`סופי-{case}.docx` בתיקיית exports).
    max_chars: 0=הכל, אחרת חיתוך לאורך הנתון. שימושי ל-Hermes Knowledge Curator."""
    return await cases.case_get_final_text(case_number, max_chars)


# Precedent attachments (user-supplied legal support for the compose phase)
@mcp.tool()
async def precedent_attach(
    case_number: str,
    quote: str,
    citation: str,
    section_id: str = "",
    chair_note: str = "",
    pdf_document_id: str = "",
) -> str:
    """צירוף פסיקה תומכת לתיק. section_id ריק = כללי לתיק; אחרת threshold_1/issue_3."""
    return await precedents.precedent_attach(
        case_number, quote, citation, section_id, chair_note, pdf_document_id,
    )


@mcp.tool()
async def precedent_list(case_number: str) -> str:
    """רשימת כל הפסיקות שצורפו לתיק."""
    return await precedents.precedent_list(case_number)


@mcp.tool()
async def precedent_remove(precedent_id: str) -> str:
    """הסרת פסיקה מצורפת."""
    return await precedents.precedent_remove(precedent_id)


@mcp.tool()
async def search_case_precedents(
    query: str, practice_area: str = "", limit: int = 10,
) -> str:
    """חיפוש בציטוטי-פסיקה שדפנה צירפה ידנית לתיקים (טבלת case_precedents) —
    קורפוס "case-attached". זה **לא** ספריית-הפסיקה הסמכותית.

    GAP-49 (INV-TOOL2): שם קודם היה `precedent_search_library` — הפוך וכמעט-זהה
    ל-`search_precedent_library` (הספרייה הסמכותית), מה שסיכן ציטוט מהמקור הלא-נכון.
    אל תצטט מכאן כמקור-סמכות ל-CREAC; לזה השתמש ב-`search_precedent_library`.

    Args:
        query: מחרוזת חיפוש (מול citation ו-quote)
        practice_area: סינון תחום משפטי (אופציונלי)
        limit: תקרת תוצאות
    """
    return await precedents.search_case_precedents(query, practice_area, _clamp_limit(limit))


@mcp.tool()
async def precedent_search_library(
    query: str, practice_area: str = "", limit: int = 10,
) -> str:
    """DEPRECATED (GAP-49) — שם-מטעה. השתמש ב-`search_case_precedents` (ציטוטים
    מצורפים-לתיק) או ב-`search_precedent_library` (ספריית-הפסיקה הסמכותית).
    Alias זמני לתאימות-לאחור — מנתב ל-search_case_precedents."""
    return await precedents.search_case_precedents(query, practice_area, _clamp_limit(limit))


# ── External Precedent Library — authoritative case-law corpus ─────
# Distinct from precedent_search_library above (chair-attached quotes)
# and from search_decisions (Daphna's style corpus).


@mcp.tool()
async def precedent_library_upload(
    file_path: str,
    citation: str,
    case_name: str = "",
    court: str = "",
    decision_date: str = "",
    source_type: str = "",
    precedent_level: str = "",
    practice_area: str = "",
    appeal_subtype: str = "",
    subject_tags: list[str] | None = None,
    is_binding: bool = True,
    headnote: str = "",
    summary: str = "",
) -> str:
    """העלאת פסיקה חיצונית (פס"ד / החלטה של ועדה אחרת) לקורפוס הסמכותי. מחלץ הלכות אוטומטית — כולן ממתינות לאישור היו"ר. practice_area: rishuy_uvniya / betterment_levy / compensation_197."""
    return await plib.precedent_library_upload(
        file_path, citation, case_name, court, decision_date,
        source_type, precedent_level, practice_area, appeal_subtype,
        subject_tags, is_binding, headnote, summary,
    )


@mcp.tool()
async def precedent_library_list(
    practice_area: str = "",
    court: str = "",
    precedent_level: str = "",
    source_type: str = "",
    search: str = "",
    source_kind: str = "external_upload",
    limit: int = 100,
) -> str:
    """רשימת הפסיקה בקורפוס, עם פילטרים.

    source_kind: 'external_upload' (ברירת מחדל — פס"ד בתי משפט) /
    'internal_committee' (החלטות ועדות ערר ערר/בל"מ שהועלו) /
    'all_committees' (שתיהן — internal + appeals_committee).
    החלטות ערר/בל"מ שמעלים נשמרות כ-internal_committee — כדי לראותן
    ברשימה השתמש ב-source_kind='internal_committee' או 'all_committees'.
    """
    return await plib.precedent_library_list(
        practice_area, court, precedent_level, source_type, search,
        source_kind, _clamp_limit(limit),
    )


@mcp.tool()
async def precedent_library_get(case_law_id: str) -> str:
    """פסיקה ספציפית בקורפוס + רשימת ההלכות שחולצו ממנה (כולל ממתינות לאישור)."""
    return await plib.precedent_library_get(case_law_id)


@mcp.tool()
async def precedent_library_delete(case_law_id: str) -> str:
    """מחיקת פסיקה מהקורפוס (cascade: chunks + halachot)."""
    return await plib.precedent_library_delete(case_law_id)


@mcp.tool()
async def precedent_link_cases(
    case_law_id_a: str,
    case_law_id_b: str,
    relation_type: str = "same_case_chain",
) -> str:
    """קישור שתי פסיקות כקשורות (דו-כיווני, idempotent). relation_type: same_case_chain | overruled_by | distinguished."""
    return await plib.precedent_link_cases(case_law_id_a, case_law_id_b, relation_type)


@mcp.tool()
async def precedent_unlink_cases(case_law_id_a: str, case_law_id_b: str) -> str:
    """הסרת קישור בין שתי פסיקות (דו-כיווני)."""
    return await plib.precedent_unlink_cases(case_law_id_a, case_law_id_b)


@mcp.tool()
async def precedent_extract_halachot(case_law_id: str) -> str:
    """הרצה מחדש של חילוץ הלכות לפסיקה קיימת. ההלכות הקיימות נמחקות, החדשות חוזרות לסטטוס pending_review."""
    return await plib.precedent_extract_halachot(case_law_id)


@mcp.tool()
async def precedent_extract_metadata(case_law_id: str) -> str:
    """חילוץ מטא-דאטה (case_name קצר, summary, headnote, key_quote, subject_tags, appeal_subtype, date, level, court, source_type) מהטקסט. ממלא רק שדות ריקים."""
    return await plib.precedent_extract_metadata(case_law_id)


@mcp.tool()
async def precedent_reindex(case_law_id: str) -> str:
    """re-chunk + re-embed פסיקה קיימת מה-full_text השמור (FU-3/GAP-09). אינו מריץ OCR/LLM — רק chunking + voyage embeddings. idempotent."""
    return await plib.precedent_reindex(case_law_id)


@mcp.tool()
async def style_corpus_enrich(corpus_id: str, overwrite: bool = False) -> str:
    """חילוץ מטא-דאטה (summary, outcome, key_principles, appeal_subtype) להחלטה בקורפוס הסגנון של דפנה. ברירת מחדל: ממלא רק שדות ריקים. שלח `overwrite=true` כדי לרענן."""
    return await train_tools.extract_decision_metadata(corpus_id, overwrite=overwrite)


@mcp.tool()
async def style_corpus_pending_enrichment(limit: int = 50) -> str:
    """רשימת החלטות בקורפוס הסגנון שעדיין חסרות summary/outcome/key_principles — מועמדות לחילוץ."""
    return await train_tools.list_corpus_pending_enrichment(_clamp_limit(limit))


@mcp.tool()
async def extraction_status() -> str:
    """סטטוס תור-החילוץ — כמה פסיקות ממתינות לחילוץ metadata/halacha + גיל הבקשה הוותיקה. read-only (חושף את התור ש-precedent_process_pending מרוקן)."""
    return await plib.extraction_status()


@mcp.tool()
async def precedent_process_pending(kind: str = "metadata", limit: int = 20) -> str:
    """ריקון תור בקשות חילוץ שנשלחו מ-UI. kind: 'metadata' או 'halacha'. מריץ extractor מקומית עם CLI על כל פריט בתור, ומנקה את הסימון אחרי הצלחה."""
    return await plib.precedent_process_pending(kind, _clamp_limit(limit))


@mcp.tool()
async def search_precedent_library(
    query: str,
    practice_area: str = "",
    court: str = "",
    precedent_level: str = "",
    appeal_subtype: str = "",
    subject_tag: str = "",
    limit: int = 10,
    include_halachot: bool = True,
) -> str:
    """חיפוש סמנטי בקורפוס הפסיקה הסמכותית. מחזיר הלכות (מאושרות בלבד) + קטעי טקסט. השתמש כש-legal-writer צריך לצטט פסיקה מחייבת בבלוק י (CREAC: rule + explanation)."""
    return await plib.search_precedent_library(
        query, practice_area, court, precedent_level, appeal_subtype,
        None, subject_tag, _clamp_limit(limit), include_halachot,
    )


# Digests radar (X12) — secondary discovery layer; NOT a citation corpus.
@mcp.tool()
async def digest_upload(
    file_path: str,
    yomon_number: str = "",
    digest_date: str = "",
    practice_area: str = "",
    appeal_subtype: str = "",
    subject_tags: list[str] | None = None,
) -> str:
    """העלאת יומון ("כל יום") לקורפוס-הגילוי (radar) + חילוץ מטא-דאטה אוטומטי. היומון הוא מקור-משני המצביע על הפסק המקורי — אינו מצוטט בהחלטה ואינו מחלץ הלכות (INV-DIG1/2). practice_area: rishuy_uvniya / betterment_levy / compensation_197."""
    return await digest_tools.digest_upload(
        file_path, yomon_number, digest_date, practice_area,
        appeal_subtype, subject_tags,
    )


@mcp.tool()
async def digest_list(
    practice_area: str = "",
    concept_tag: str = "",
    linked: bool | None = None,
    search: str = "",
    limit: int = 100,
) -> str:
    """רשימת יומונים בקורפוס-הגילוי, עם פילטרים. linked=false → יומונים שהפסק המקורי שלהם עוד לא נקלט לספריית הפסיקה (פער-ידע גלוי, INV-DIG3)."""
    return await digest_tools.digest_list(
        practice_area, concept_tag, linked, search, _clamp_limit(limit),
    )


@mcp.tool()
async def digest_get(digest_id: str) -> str:
    """יומון ספציפי לפי מזהה (כולל מראה-מקום, ניתוח, וקישור לפסק המקורי אם קיים)."""
    return await digest_tools.digest_get(digest_id)


@mcp.tool()
async def digest_link(digest_id: str, case_law_id: str) -> str:
    """קישור ידני של יומון לפסק הדין המקורי בספריית הפסיקה (INV-DIG3). idempotent."""
    return await digest_tools.digest_link(digest_id, case_law_id)


@mcp.tool()
async def digest_relink(digest_id: str) -> str:
    """ניסיון-קישור מחדש: בודק אם פסק הדין המקורי של היומון כבר בספרייה ומקשר אוטומטית."""
    return await digest_tools.digest_relink(digest_id)


@mcp.tool()
async def digest_delete(digest_id: str) -> str:
    """מחיקת יומון מקורפוס-הגילוי."""
    return await digest_tools.digest_delete(digest_id)


@mcp.tool()
async def search_digests(
    query: str,
    practice_area: str = "",
    subject_tag: str = "",
    concept_tag: str = "",
    limit: int = 10,
) -> str:
    """חיפוש סמנטי בקורפוס-הגילוי (יומוני "כל יום") — מצפן-מחקר (radar). מחזיר את היומון הרלוונטי + מראה-המקום של הפסק המקורי. ⚠️ היומון אינו מצוטט בהחלטה (INV-DIG1) — הצטט מהפסק המקורי דרך search_precedent_library. החוקר משתמש בזה בשלב 2ב.0 לפני האימות."""
    return await digest_tools.search_digests(
        query, practice_area, subject_tag, concept_tag, _clamp_limit(limit),
    )


@mcp.tool()
async def digest_process_pending(limit: int = 20) -> str:
    """ריקון תור היומונים שהועלו מה-UI וממתינים לעיבוד-LLM מקומי. מריץ חילוץ-מטא-דאטה + embedding + autolink על כל יומון 'pending' (מקומית עם CLI). חלופת-MCP ל-scripts/ingest_digests_batch.py."""
    return await digest_tools.digest_process_pending(_clamp_limit(limit))


@mcp.tool()
async def digest_radar(case_number: str, limit: int = 5, min_score: float = 0.45) -> str:
    """רדאר-יומונים הקשרי-לתיק (X12) — מחזיר יומונים לא-מקושרים (פס"ד שעוד אין לנו) שהנושא שלהם קרוב סמנטית לתיק, כדי שלא ייפול פס"ד רלוונטי בזמן ההכרעה. כל ליד מצביע על מראה-המקום של הפס"ד המקורי + סטטוס-הפער + פעולה מוצעת. ⚠️ radar בלבד — היומון אינו מצוטט (INV-DIG1)."""
    return await digest_tools.digest_radar(case_number, _clamp_limit(limit), min_score)


@mcp.tool()
async def halacha_review(
    halacha_id: str,
    status: str,
    reviewer: str = "דפנה",
    rule_statement: str = "",
    reasoning_summary: str = "",
    subject_tags: list[str] | None = None,
    practice_areas: list[str] | None = None,
    canonical_statement: str = "",
) -> str:
    """אישור / דחייה / עריכה של הלכה שחולצה אוטומטית. status: pending_review / approved / rejected / published.
    canonical_statement: עריכת ניסוח העיקרון הקנוני הרחב (V41)."""
    return await plib.halacha_review(
        halacha_id, status, reviewer, rule_statement, reasoning_summary,
        subject_tags, practice_areas, canonical_statement,
    )


@mcp.tool()
async def halachot_pending(
    limit: int = 100,
    include_low_quality: bool = False,
    instance_type: str = "original",
) -> str:
    """תור ההלכות הממתינות לאישור. V41: ברירת-מחדל instance_type='original' (עקרונות חדשים בלבד, לא ציטוטים)."""
    return await plib.halachot_pending(_clamp_limit(limit), include_low_quality, instance_type)


@mcp.tool()
async def canonical_halacha_list(
    practice_area: str = "",
    review_status: str = "",
    limit: int = 50,
    offset: int = 0,
) -> str:
    """רשימת עקרונות קנוניים (canonical_halachot). V41.
    practice_area: סינון תחום עיסוק. review_status: pending_synthesis/pending_review/approved/published."""
    return await plib.canonical_halacha_list(practice_area, review_status, limit, offset)


@mcp.tool()
async def canonical_halacha_get(canonical_id: str) -> str:
    """שלוף עיקרון קנוני + כל האינסטנסים שלו לפי פסיקה. V41."""
    return await plib.canonical_halacha_get(canonical_id)


@mcp.tool()
async def canonical_synthesize_pending(limit: int = 20) -> str:
    """סנתז ניסוח-קנוני לעקרונות הממתינים (pending_synthesis) → pending_review (שער-יו"ר). V41 Phase 4.
    מעוגן בציטוטי-המופעים (INV-AH) עם שער-drift. on-demand/burst; המסה הראשונית ב-backfill."""
    return await plib.canonical_synthesize_pending(limit)


# Documents
@mcp.tool()
async def document_upload(
    case_number: str,
    file_path: str,
    doc_type: str = "appeal",
    title: str = "",
) -> str:
    """העלאה ועיבוד מסמך לתיק ערר (PDF/DOCX/RTF/TXT). מחלץ טקסט ויוצר embeddings."""
    return await documents.document_upload(case_number, file_path, doc_type, title)


@mcp.tool()
async def document_upload_training(
    file_path: str,
    decision_number: str = "",
    decision_date: str = "",
    subject_categories: list[str] | None = None,
    title: str = "",
    practice_area: str = "appeals_committee",
    appeal_subtype: str = "",
) -> str:
    """העלאת החלטה קודמת של דפנה לקורפוס הסגנון. קטגוריות: בנייה, שימוש חורג, תכנית, היתר, הקלה, חלוקה, תמ"א 38, היטל השבחה, פיצויים 197. סוג ערר: building_permit / betterment_levy / compensation_197 (ריק = אוטומטי ממספר ההחלטה)."""
    return await documents.document_upload_training(
        file_path, decision_number, decision_date, subject_categories, title,
        practice_area, appeal_subtype,
    )


@mcp.tool()
async def document_get_text(case_number: str, doc_title: str = "") -> str:
    """קבלת טקסט מלא של מסמך מתוך תיק."""
    return await documents.document_get_text(case_number, doc_title)


@mcp.tool()
async def document_list(case_number: str) -> str:
    """רשימת מסמכים בתיק."""
    return await documents.document_list(case_number)


@mcp.tool()
async def document_update(
    case_number: str,
    doc_id: str,
    doc_type: str = "",
    appraiser_side: str = "",
) -> str:
    """עדכון תיוג מסמך — doc_type ו/או appraiser_side (committee/appellant/deciding). ריק = ללא שינוי."""
    return await documents.document_update(case_number, doc_id, doc_type, appraiser_side)


# Claims extraction
@mcp.tool()
async def extract_claims(
    case_number: str,
    doc_title: str = "",
    party_hint: str = "",
) -> str:
    """חילוץ טענות מכתב טענות בתיק. מחלץ טענות לפי צד ושומר ב-DB."""
    return await documents.extract_claims(case_number, doc_title, party_hint)


@mcp.tool()
async def get_claims(
    case_number: str,
    party_role: str = "",
) -> str:
    """שליפת טענות שחולצו לתיק. party_role: appellant/respondent/committee/permit_applicant (ריק=הכל)."""
    return await documents.get_claims(case_number, party_role)


# Legal arguments — aggregated (de-duped) propositions
@mcp.tool()
async def aggregate_claims_to_arguments(
    case_number: str,
    force: bool = False,
) -> str:
    """כינוס פרופוזיציות גולמיות (claims) לטיעונים משפטיים מובחנים — ~6-12 לכל צד.

    משתמש ב-Claude headless לסיווג ואיגוד. force=True מוחק טיעונים קיימים לפני חישוב מחדש.
    """
    return await la_tools.aggregate_claims_to_arguments(case_number, force=force)


@mcp.tool()
async def get_legal_arguments(
    case_number: str,
    party: str = "",
) -> str:
    """שליפת טיעונים משפטיים מאוגדים. party: appellant/respondent/committee/permit_applicant (ריק=הכל)."""
    return await la_tools.get_legal_arguments(case_number, party)


@mcp.tool()
async def reanalyze_claims(
    case_number: str,
    reanalyze_all_primary: bool = False,
) -> str:
    """ניתוח-מחדש מאחד אחרי הוספת מסמך-עיקרי: מחלץ ממסמכים-עיקריים חדשים/לא-נותחו בלבד,
    מאחד עם הטענות הקיימות (לא מוחק הכול), מריץ צבירה-מחדש, ומחזיר בדיקת-השפעה
    (diff טיעונים/המלצה לפני↔אחרי) ליו"ר. reanalyze_all_primary=True = כל המסמכים-העיקריים.
    """
    return await la_tools.reanalyze_claims(case_number, reanalyze_all_primary)


# References
@mcp.tool()
async def extract_references(
    case_number: str,
    doc_title: str = "",
) -> str:
    """זיהוי תכניות, פסיקה וחקיקה מתוך מסמכי תיק. מזהה ומקשר להפניות קיימות ב-DB."""
    return await documents.extract_references(case_number, doc_title)


# Search
@mcp.tool()
async def search_decisions(
    query: str,
    limit: int = 10,
    section_type: str = "",
    practice_area: str = "",
    appeal_subtype: str = "",
    case_number: str = "",
) -> str:
    """חיפוש סמנטי בהחלטות קודמות ובמסמכים — מסונן לפי תחום משפטי."""
    return await search.search_decisions(
        query, _clamp_limit(limit), section_type, practice_area, appeal_subtype, case_number,
    )


@mcp.tool()
async def search_case_documents(
    case_number: str,
    query: str,
    limit: int = 10,
) -> str:
    """חיפוש סמנטי בתוך מסמכי תיק ספציפי."""
    return await search.search_case_documents(case_number, query, _clamp_limit(limit))


@mcp.tool()
async def find_similar_cases(
    description: str,
    limit: int = 5,
    practice_area: str = "",
    appeal_subtype: str = "",
    case_number: str = "",
) -> str:
    """מציאת תיקים דומים על בסיס תיאור — מסונן לפי תחום משפטי."""
    return await search.find_similar_cases(
        description, _clamp_limit(limit), practice_area, appeal_subtype, case_number,
    )


@mcp.tool()
async def search_internal_decisions(
    query: str,
    practice_area: str = "",
    appeal_subtype: str = "",
    district: str = "",
    chair_name: str = "",
    limit: int = 10,
    include_halachot: bool = True,
    include_cited_by: bool = False,
) -> str:
    """חיפוש בהחלטות ועדות ערר לתכנון ובנייה (כל המחוזות).

    מחזיר החלטות מהקורפוס הפנימי של ועדות הערר — נפרד מפסיקת בתי המשפט.
    השתמש בו במקביל ל-search_precedent_library להצגת שתי שכבות נפרדות.

    Args:
        query: שאילתת חיפוש בעברית
        practice_area: rishuy_uvniya / betterment_levy / compensation_197
        appeal_subtype: סינון לפי תת-סוג ערר
        district: מחוז — ירושלים / מרכז / תל אביב / צפון / דרום / ארצי. ריק = כל המחוזות
        chair_name: שם יו"ר הוועדה לסינון. ריק = כל היו"רים
        limit: מספר תוצאות מקסימלי
        include_halachot: האם לכלול הלכות שחולצו
        include_cited_by: True = הוסף תוצאות עקיפות — לכל hit הוסף גם החלטות
            שהוא מצטט (מתוך citation graph). שימושי לחיפוש "כל הקשור ל-X"
            כשרוצים להרחיב מעבר לטקסט המקורי. default False.
    """
    return await search.search_internal_decisions(
        query, practice_area, appeal_subtype, district, chair_name, _clamp_limit(limit), include_halachot,
        include_cited_by=include_cited_by,
    )


# Drafting
@mcp.tool()
async def get_style_guide() -> str:
    """שליפת דפוסי הסגנון של דפנה - נוסחאות, ביטויים אופייניים ומבנה."""
    return await drafting.get_style_guide()


@mcp.tool()
async def style_distance(case_number: str) -> str:
    """מדד מרחק-סגנון (T7) — האם הטיוטה מתכנסת לסגנון דפנה: סטיית יחסי-זהב,
    ספירת אנטי-דפוסים, ושיעור-השינוי draft→final מפנקס-ההתאמה. ללא LLM."""
    import json as _json
    from legal_mcp.services import style_distance as _sd
    result = await _sd.style_distance(case_number)
    return _json.dumps(result, ensure_ascii=False, indent=2)


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


@mcp.tool()
async def get_chair_directions(case_number: str) -> str:
    """שליפת עמדות יו"ר הוועדה (דפנה) על סוגיות הערר כ-direction_doc לכותב.
    קורא מ-analysis-and-research.md שמולא ע"י דפנה דרך ה-UI.
    מחזיר סטטוס (missing/empty/partial/complete) + עמדות מובנות.
    """
    return await drafting.get_chair_directions(case_number)


@mcp.tool()
async def get_decision_template(case_number: str) -> str:
    """תבנית מבנית להחלטה מלאה עם פרטי התיק."""
    return await drafting.get_decision_template(case_number)


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


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


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


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


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


@mcp.tool()
async def get_appraiser_facts(case_number: str) -> str:
    """קריאת עובדות-השמאי שכבר חולצו (facts + סתירות) — ללא חילוץ-מחדש יקר. ה-get המקביל ל-extract_appraiser_facts."""
    return await drafting.get_appraiser_facts(case_number)


# ── Protocol comparative analysis (WS4 / #203) — ניתוח פרוטוקול ────

@mcp.tool()
async def analyze_protocol(case_number: str, document_id: str = "") -> str:
    """ניתוח השוואתי של פרוטוקול-דיון מול כתבי-הטענות: אילו טענות ירדו/חוזקו/עלו-חדשות + חידוד-שאלות + חילוץ א–ד.

    בוחר אוטומטית פרוטוקול ועדת-ערר (scope שאינו 'lower'); document_id מכוון לדיון
    מדויק כשיש כמה פרוטוקולים. מזין ל"ידע-התיק" (protocol_analysis); דורש פרוטוקול
    doc_type='protocol' + טיעונים מאוגדים. Claude מקומי Opus 4.8 effort=high;
    re-run מחליף (idempotent).
    """
    return await drafting.analyze_protocol(case_number, document_id=document_id)


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

    change_type: dropped/strengthened/newly_raised (ריק=הכל).
    """
    return await drafting.get_protocol_analysis(case_number, change_type)


# ── Planning-schemes registry (V38) — מרשם-התכניות ─────────────────
# SSOT לזהות+תוקף של תכנית, נעשה שימוש חוזר בין תיקים (G2). פלט-LLM נכנס
# pending_review וממתין לאישור-יו"ר (plan_review, G10) לפני שמשמש בבלוק ט.

@mcp.tool()
async def extract_plans(case_number: str) -> str:
    """חילוץ תכניות ותוקפן מכל מסמכי התיק אל מרשם-התכניות (pending_review). ה-extract היקר."""
    return await plans_tools.extract_plans(case_number)


@mcp.tool()
async def plan_fetch(plan_number: str) -> str:
    """משיכת זהות+תוקף של תב"ע מ-מנהל-התכנון (mavat) — מועמד-לאישור, לא כתיבה. כל ערך נושא source_url (INV-AH)."""
    return await plans_tools.plan_fetch(plan_number)


@mcp.tool()
async def plan_get(plan_number: str) -> str:
    """קריאת תכנית מהמרשם לפי מספר (מנורמל; נופל ל-alias). ה-get הזול."""
    return await plans_tools.plan_get(plan_number)


@mcp.tool()
async def plan_search(query: str, limit: int = 20) -> str:
    """חיפוש fuzzy במרשם-התכניות לפי מספר/שם/ייעוד."""
    return await plans_tools.plan_search(query, limit)


@mcp.tool()
async def plan_list(review_status: str = "", limit: int = 500) -> str:
    """רשימת תכניות במרשם, אופציונלית מסוננת לפי review_status (תור-אישור)."""
    return await plans_tools.plan_list(review_status, limit)


@mcp.tool()
async def plan_upsert(
    plan_number: str,
    display_name: str = "",
    plan_type: str = "",
    gazette_date: str = "",
    yalkut_number: str = "",
    purpose: str = "",
    review_status: str = "approved",
    aliases: str = "",
) -> str:
    """כתיבה/עריכה ידנית מבוקרת של תכנית במרשם (ברירת-מחדל approved — קלט-יו"ר). מנרמל בכתיבה (G1)."""
    return await plans_tools.plan_upsert(
        plan_number, display_name, plan_type, gazette_date,
        yalkut_number, purpose, review_status, aliases,
    )


@mcp.tool()
async def plan_review(plan_id: str, status: str) -> str:
    """שער-היו"ר (G10): אישור/דחייה/איפוס של תכנית במרשם (approved/rejected/pending_review)."""
    return await plans_tools.plan_review(plan_id, status)


@mcp.tool()
async def write_interim_draft(case_number: str, instructions: str = "") -> str:
    """כתיבת ארבעת הבלוקים לטיוטת ביניים (רקע, תכניות+היתרים, טענות, הליכים) — אותו skill וטמפלט."""
    return await drafting.write_interim_draft(case_number, instructions)


@mcp.tool()
async def export_interim_draft(case_number: str, output_path: str = "") -> str:
    """ייצוא טיוטת ביניים ל-DOCX — סדר חדש (רקע → תכניות+היתרים → טענות → הליכים), ללא דיון/סיכום."""
    return await drafting.export_interim_draft(case_number, output_path)


@mcp.tool()
async def summarize_party_claims(case_number: str, instructions: str = "") -> str:
    """סיכום-מנהלים מזוקק של טענות הצדדים — מסמך פרוזה נפרד מטיוטת-ההחלטה, להכנת היו"ר לדיון. נגזר מ-legal_arguments/claims (אותו מקור), נעוץ Opus 4.8, נשמר וניתן-לייצוא."""
    return await drafting.summarize_party_claims(case_number, instructions)


@mcp.tool()
async def apply_user_edit(case_number: str, edit_filename: str) -> str:
    """רישום עריכה שהעלה המשתמש (עריכה-v*.docx) כמקור האמת החדש — מזריק bookmarks אם חסר."""
    return await drafting.apply_user_edit(case_number, edit_filename)


@mcp.tool()
async def list_bookmarks(case_number: str) -> str:
    """רשימת bookmarks הקיימים ב-active_draft של התיק (אנקורים ל-revisions)."""
    return await drafting.list_bookmarks(case_number)


@mcp.tool()
async def revise_draft(case_number: str, revisions_json: str,
                       author: str = "מערכת AI") -> str:
    """החלת revisions (Track Changes) על ה-active_draft, יוצר טיוטה-v{N+1}.docx חדשה."""
    return await drafting.revise_draft(case_number, revisions_json, author)


@mcp.tool()
async def analyze_style(appeal_subtype: str = "") -> str:
    """ניתוח סגנון על קורפוס ההחלטות של דפנה. מחלץ ושומר דפוסי כתיבה. סוג ערר: building_permit / betterment_levy / compensation_197 (ריק = הכל)."""
    return await drafting.analyze_style(appeal_subtype)


@mcp.tool()
async def write_block(
    case_number: str,
    block_id: str,
    instructions: str = "",
) -> str:
    """כתיבת בלוק יחיד בהחלטה: block-alef עד block-yod-bet. שומר ב-DB."""
    return await drafting.write_block(case_number, block_id, instructions)


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


# Workflow
@mcp.tool()
async def workflow_status(case_number: str) -> str:
    """סטטוס תהליך עבודה מלא לתיק."""
    return await workflow.workflow_status(case_number)


@mcp.tool()
async def get_metrics(case_number: str = "") -> str:
    """מדדי הצלחה — KPIs לתיק ספציפי או דשבורד כולל. ריק = דשבורד."""
    return await workflow.get_metrics(case_number)


@mcp.tool()
async def processing_status() -> str:
    """סטטוס כללי - מספר תיקים, מסמכים, chunks."""
    return await workflow.processing_status()


# Outcome & Brainstorming
@mcp.tool()
async def set_outcome(
    case_number: str,
    outcome: str,
    reasoning: str = "",
) -> str:
    """הזנת תוצאה לתיק: rejection (דחייה), partial_acceptance (קבלה חלקית), full_acceptance (קבלה מלאה). ערכי-legacy ממופים. אם אין נימוק — מפעיל סיעור מוחות."""
    return await workflow.set_outcome(case_number, outcome, reasoning)


@mcp.tool()
async def brainstorm_directions(case_number: str) -> str:
    """סיעור מוחות — הצגת טענות מרכזיות והצעת 2-3 כיוונים אפשריים לנימוק ההחלטה."""
    return await workflow.brainstorm_directions(case_number)


@mcp.tool()
async def approve_direction(
    case_number: str,
    direction_index: int = 0,
    additional_notes: str = "",
) -> str:
    """אישור כיוון — יוצר מסמך כיוון מאושר. חובה לפני כתיבת דיון (בלוק י)."""
    return await workflow.approve_direction(case_number, direction_index, additional_notes)


@mcp.tool()
async def ingest_final_version(
    case_number: str,
    file_path: str = "",
    final_text: str = "",
) -> str:
    """קליטת גרסה סופית שדפנה חתמה — השוואה לטיוטה וחילוץ לקחים לשיפור עתידי."""
    return await workflow.ingest_final_version(case_number, file_path, final_text)


@mcp.tool()
async def internal_decision_migrate(
    source: str = "both",
    dry_run: bool = True,
) -> str:
    """העברת החלטות ועדת ערר קיימות לקורפוס הפנימי (פעולת admin).

    source: 'style_corpus' | 'external_corpus' | 'both'
    dry_run: אם true — מציג מה יקרה ללא כתיבה
    """
    import json as _json
    from legal_mcp.services import internal_decisions as int_svc
    if source not in {"style_corpus", "external_corpus", "both"}:
        return "source חייב להיות style_corpus / external_corpus / both"
    results: dict = {}
    if source in {"style_corpus", "both"}:
        results["style_corpus"] = await int_svc.migrate_from_style_corpus(dry_run=dry_run)
    if source in {"external_corpus", "both"}:
        results["external_corpus"] = await int_svc.migrate_from_external_corpus(dry_run=dry_run)
    return _json.dumps(results, ensure_ascii=False, indent=2)


@mcp.tool()
async def internal_decision_enrich(
    dry_run: bool = True,
) -> str:
    """העשרת החלטות שהומגרו (חד-פעמי): תיקון מספר ערר + שם + תאריך + תור להלכות.

    dry_run=True — מציג כמה רשומות יטופלו ללא כתיבה.
    dry_run=False — מריץ בפועל: metadata extraction (תיקון case_number/case_name/date) ואחר כך תור חילוץ הלכות.
    """
    import json as _json
    from legal_mcp.services import internal_decisions as int_svc
    result = await int_svc.enrich_migrated_entries(dry_run=dry_run)
    return _json.dumps(result, ensure_ascii=False, indent=2)


@mcp.tool()
async def internal_decision_upload(
    file_path: str,
    case_number: str,
    chair_name: str,
    district: str,
    case_name: str = "",
    court: str = "",
    decision_date: str = "",
    practice_area: str = "",
    appeal_subtype: str = "",
    subject_tags: list[str] | None = None,
    summary: str = "",
    is_binding: bool = False,
) -> str:
    """העלאת החלטה של ועדת ערר (internal_committee) לקורפוס הסמכותי.

    שדות חובה: file_path, case_number, chair_name, district.
    שמירת ההחלטה עוברת דרך ingest_internal_decision — תויג source_kind='internal_committee' אוטומטית.
    district תקין: ירושלים / מרכז / תל אביב / צפון / דרום / חיפה / ארצי.

    בניגוד ל-precedent_library_upload (שתמיד שומר external_upload),
    הכלי הזה הוא הנתיב המוסמך להחלטות ועדת ערר ומכריח chair_name+district.
    """
    return await int_tools.internal_decision_upload(
        file_path=file_path,
        case_number=case_number,
        chair_name=chair_name,
        district=district,
        case_name=case_name,
        court=court,
        decision_date=decision_date,
        practice_area=practice_area,
        appeal_subtype=appeal_subtype,
        subject_tags=subject_tags,
        summary=summary,
        is_binding=is_binding,
    )


# ── Missing precedents (TaskMaster #35) ───────────────────────────


@mcp.tool()
async def missing_precedent_create(
    citation: str,
    case_number: str = "",
    cited_in_document_id: str = "",
    cited_by_party: str = "unknown",
    cited_by_party_name: str = "",
    legal_topic: str = "",
    legal_issue: str = "",
    claim_quote: str = "",
    case_name: str = "",
    notes: str = "",
) -> str:
    """תיעוד פסיקה שצוטטה בכתבי הטענות אך אינה בקורפוס.

    שימוש: סוכן המחקר (legal-researcher) קורא לזה כשהוא מזהה ציטוט שלא
    ניתן לאמת מול הקורפוס. הרשומה נשארת 'open' עד שהיו"ר מעלה את הפסיקה.
    cited_by_party: appellant / respondent / committee / permit_applicant / unknown.
    דה-דופ אוטומטי: ציטוט+תיק זהים → מחזיר את הרשומה הקיימת.
    """
    return await mp_tools.missing_precedent_create(
        citation=citation,
        case_number=case_number,
        cited_in_document_id=cited_in_document_id,
        cited_by_party=cited_by_party,
        cited_by_party_name=cited_by_party_name,
        legal_topic=legal_topic,
        legal_issue=legal_issue,
        claim_quote=claim_quote,
        case_name=case_name,
        notes=notes,
    )


@mcp.tool()
async def missing_precedent_list(
    case_number: str = "",
    status: str = "open",
    legal_topic: str = "",
    limit: int = 50,
) -> str:
    """רשימת פסיקות חסרות לתיק או בכלל. status: open/uploaded/closed/irrelevant.

    שימוש: היו"ר רואה מה ממתין להעלאה; הסוכן מאשר שלא יוצר כפילויות.
    """
    return await mp_tools.missing_precedent_list(
        case_number=case_number,
        status=status,
        legal_topic=legal_topic,
        limit=_clamp_limit(limit),
    )


@mcp.tool()
async def missing_precedent_close(
    id: str,
    linked_case_law_id: str = "",
    notes: str = "",
    status: str = "closed",
) -> str:
    """סגירת רשומת פסיקה חסרה לאחר העלאה לקורפוס.

    status: closed (הועלה ונקשר) / uploaded (הועלה, ממתין לקישור) /
    irrelevant (היו"ר החליט שזה לא רלוונטי לקורפוס).
    """
    return await mp_tools.missing_precedent_close(
        id=id,
        linked_case_law_id=linked_case_law_id,
        notes=notes,
        status=status,
    )


# ── Court verdict auto-fetch (X13) ────────────────────────────────
@mcp.tool()
async def court_verdict_fetch(citation: str) -> str:
    """אחזור אוטומטי של פסק-דין בית-משפט מנט המשפט/פורטל-העליון וקליטה לקורפוס.

    מסווג את הציטוט (עליון→Tier0 / מנהלי→Tier1 / ערר→skip), מוריד וקולט דרך
    צינור-הקליטה הקנוני. דוגמה: 'עת"מ 46111-12-22'. כלי מקומי בלבד."""
    return await cf_tools.court_verdict_fetch(citation)


@mcp.tool()
async def court_fetch_status(case_number: str = "", status_filter: str = "") -> str:
    """סטטוס תור-אחזור הפסיקה. case_number לפריט יחיד, או status_filter (pending/failed/manual/done)."""
    return await cf_tools.court_fetch_status(case_number, status_filter)


@mcp.tool()
async def court_fetch_drain(limit: int = 10) -> str:
    """ריקון תור-אחזור הפסיקה — מוריד וקולט jobs ממתינים שהיומונים מילאו, וקושר חזרה ליומון. מקומי בלבד."""
    return await cf_tools.court_fetch_drain(limit)


# ── Internal citations graph (TaskMaster #34) ─────────────────────


@mcp.tool()
async def extract_internal_citations(
    case_law_id: str = "",
    chair_name: str = "",
    limit: int = 0,
) -> str:
    """חילוץ ציטוטים פנימיים מהחלטות ועדת ערר ושמירה ב-citation graph.

    משתמש בדפוסי regex עבריים ("ונפנה ל…", "כפי שקבעתי…", "ראה החלטתי…")
    לזיהוי הפניות בין החלטות. אם case_law_id סופק — מריץ על שורה אחת
    (שימושי אחרי upload). אם chair_name סופק — מריץ על כל ההחלטות של
    אותו יו"ר. אם שניהם ריקים — מריץ על כל ה-internal_committee corpus.

    איידמפוטנטי: ניתן להריץ שוב ושוב בלי כפילויות. ציטוטים שמופנים
    להחלטות שעדיין לא בקורפוס נשמרים כ-unlinked (cited_case_law_id=NULL)
    ויראו ב-list_internal_citations כשהיו"ר יחליט אם להעלות אותן.
    """
    return await cit_tools.extract_internal_citations(
        case_law_id=case_law_id,
        chair_name=chair_name,
        limit=limit,
    )


@mcp.tool()
async def list_internal_citations(
    case_law_id: str = "",
    linked_only: bool = False,
    limit: int = 50,
) -> str:
    """רשימת ציטוטים יוצאים מהחלטה (מה ההחלטה מצטטת).

    משתמש לקבלת תמונה של בסיס הפסיקה שהחלטה הסתמכה עליו.
    linked_only=True מסנן רק ציטוטים שזוהו ב-case_law של הקורפוס.
    """
    return await cit_tools.list_internal_citations(
        case_law_id=case_law_id,
        linked_only=linked_only,
        limit=_clamp_limit(limit),
    )


@mcp.tool()
async def list_incoming_citations(
    case_law_id: str = "",
    limit: int = 50,
) -> str:
    """רשימת ציטוטים נכנסים אל החלטה (אילו החלטות מצטטות אותה).

    שימוש: רוצים לדעת אילו החלטות של דפנה (או של ועדות אחרות) הסתמכו
    על פסק דין מסוים — מעבירים את ה-case_law_id של פסק הדין.
    """
    return await cit_tools.list_incoming_citations(
        case_law_id=case_law_id,
        limit=_clamp_limit(limit),
    )


@mcp.tool()
async def record_chair_feedback(
    case_number: str,
    feedback_text: str,
    block_id: str = "block-yod",
    category: str = "missing_content",
    lesson_extracted: str = "",
) -> str:
    """תיעוד הערת יו"ר (דפנה) על טיוטת החלטה — חסר, שגיאה, סגנון."""
    return await workflow.record_chair_feedback(
        case_number, feedback_text, block_id, category, lesson_extracted,
    )


@mcp.tool()
async def list_chair_feedback(
    case_number: str = "",
    category: str = "",
    unresolved_only: bool = True,
    limit: int = 100,
) -> str:
    """הצגת הערות יו"ר שתועדו — אפשר לסנן לפי תיק, קטגוריה, מטופלות."""
    return await workflow.list_chair_feedback(case_number, category, unresolved_only, _clamp_limit(limit))


@mcp.tool()
async def record_curator_findings(case_number: str, findings: list[dict]) -> str:
    """לכידת ממצאי-האוצֵר על החלטה סופית כ-decision_lessons מובְנים (source='curator',
    ממתינים לשער-יו"ר) — INV-LRN3. כל ממצא: {"text": "...", "category": style/structure/
    lexicon/tabular} (או "tag" עברי). מחזיר כמה נרשמו וכמה כפילויות דולגו. read-only על
    התוכן — הרישום הצעה הממתינה לאישור דפנה ב-/training, לא שינוי-קול ישיר (G10)."""
    return await workflow.record_curator_findings(case_number, findings)


@mcp.tool()
async def lesson_synthesize_pending(
    practice_area: str = "", category: str = "", apply: bool = True,
) -> str:
    """סינתזת-לקחים (#158 / INV-LRN8): ממזגת לקחי-סגנון חופפים ל"לקח-על" אחד עשיר (source='synthesis'),
    המקורות→superseded; הסט שזורם לכותב קטֵן ומשתבח (limit=15 מפסיק לחתוך). מעוגן-מקור (INV-AH) +
    שער-drift; שער מדורג-הפיך (G10). apply=False = dry-run. practice_area/category ריקים = כל ה-shards."""
    return await workflow.lesson_synthesize_pending(practice_area, category, apply)


@mcp.tool()
async def halacha_corroboration(halacha_id: str) -> dict:
    """החזר את ה-corroboration של הלכה: הציטוטים שמתקפים אותה, הטיפול, וסיכום (X11, read-only)."""
    from uuid import UUID
    from legal_mcp.services import corroboration as cor, db
    links = await db.list_corroboration_for_halacha(UUID(halacha_id))
    agg = cor.aggregate(
        [{"source_id": (l["citing_case_law_id"] or l["citing_decision_id"] or ""), "treatment": l["treatment"]} for l in links]
    )
    return {"halacha_id": halacha_id, "summary": agg, "citations": links}


@mcp.tool()
async def corroboration_rebuild(case_law_id: str = "") -> dict:
    """בנה/רענן את ה-corroboration ויישם אישור-אוטומטי. ריק = כל הקורפוס (backfill);
    מזהה-תקדים = תקדים בודד. כותב halacha_citation_corroboration ומעדכן review_status
    (corroborated→approved, overruled→pending_review). X11 Phase 2."""
    from legal_mcp.services import corroboration as cor
    if case_law_id.strip():
        return await cor.build_for_precedent(case_law_id.strip())
    return await cor.build_all()


def main():
    """Run the server on the transport named by ``MCP_TRANSPORT`` (default stdio).

    ONE server, two transports — deliberately not a second implementation (G2).
    The tool registry, services and DB pool above are shared verbatim; only the
    wire protocol differs, so a tool can never exist on one transport and not
    the other.

    - ``stdio`` (default) — the historical path. Every interactive Claude Code
      session reaches us this way via the ``legal-ai`` entry in ``~/.claude.json``.
      Changing this default would break them all, so it stays the default.
    - ``streamable-http`` — required by the agent-platform port. Agents driven
      over the Agent Client Protocol receive their MCP servers from the *client*
      at session start, and that channel accepts HTTP servers only: each is
      injected as ``{type:"http", name, url, headers:[Bearer]}``. A stdio server
      has no path into such a session at all, which is why platform-driven agents
      lost all 108 tools when their execution engine changed — TaskMaster #231.
      Platform-side wiring lives behind the port (``web/agent_platform_port.py``),
      not here; this module only has to be reachable over HTTP.

    Host/port come from FastMCP's own ``FASTMCP_HOST`` / ``FASTMCP_PORT`` settings.
    Bind to loopback only: this transport carries no authentication yet, and the
    registry includes destructive tools (``case_delete``, ``precedent_library_delete``).
    The Bearer gate is #231.2 and MUST land before this is reachable off-host.
    """
    transport = os.environ.get("MCP_TRANSPORT", "stdio").strip() or "stdio"
    valid = ("stdio", "sse", "streamable-http")
    if transport not in valid:
        # Fail loudly — a typo must not silently fall back to stdio and leave
        # the HTTP listener absent while everything "looks" fine (§6).
        raise SystemExit(
            f"MCP_TRANSPORT={transport!r} is not one of {valid}",
        )
    if transport != "stdio":
        logger.info(
            "Serving MCP over %s on %s:%s",
            transport, mcp.settings.host, mcp.settings.port,
        )
    mcp.run(transport=transport)


if __name__ == "__main__":
    main()
