"""
code_review.py - Stage 7: code / ADA / TDLR / Municode review (LLM + retrieval).

Flags likely code and accessibility issues, grounded in BOTH the drawing
evidence and retrieved excerpts of the applicable standards (see code_refs.py).
The model may only cite section numbers that appear in the retrieved excerpts;
with an empty corpus it runs reasoning-only and cites nothing.

Sheets are processed in batches (bounded assertion count per call) so a single
oversized whole-set call can't truncate and zero out the whole stage. Each batch
retrieves excerpts targeted to its own assertions. Emits the canonical issue
schema; returns [] on failure.
"""

from concurrent.futures import ThreadPoolExecutor
from typing import Dict, List

from backend import config
from backend.pipeline import code_refs
from backend.pipeline.jurisdiction import active_review_paths
from backend.pipeline._serialize import dumps, slim_sheets
from backend.pipeline._stage import call_stage, collect_list, validate_issue
from backend.prompts import CODE_REVIEW_SYSTEM_PROMPT, CODE_REVIEW_USER_INSTRUCTION


def _sheet_batches(sheets: List[Dict], max_assertions: int) -> List[List[Dict]]:
    """Group whole sheets so each batch holds <= max_assertions facts."""
    batches: List[List[Dict]] = []
    cur: List[Dict] = []
    n = 0
    for s in sheets:
        a = len(s.get("assertions", []))
        if cur and n + a > max_assertions:
            batches.append(cur)
            cur, n = [], 0
        cur.append(s)
        n += a
    if cur:
        batches.append(cur)
    return batches


def _review_batch(batch: List[Dict], jurisdiction: Dict, sheet_index: Dict,
                  paths: Dict) -> List[Dict]:
    assertions = [a for s in batch for a in s.get("assertions", [])]
    excerpts = code_refs.retrieve(paths, assertions)
    parsed = call_stage(
        CODE_REVIEW_SYSTEM_PROMPT,
        CODE_REVIEW_USER_INSTRUCTION,
        subs={
            "jurisdiction": dumps(jurisdiction or {}),
            "sheet_index": dumps(sheet_index or {}),
            "assertions": dumps(slim_sheets(batch)),
            "code_references": code_refs.format_excerpts(excerpts),
        },
        max_tokens=config.CODE_MAX_TOKENS,
    )
    return collect_list(parsed, "issues", lambda c: validate_issue(c, "code"))


def code_review(jurisdiction: Dict, sheets: List[Dict], sheet_index: Dict) -> List[Dict]:
    paths = active_review_paths(jurisdiction)
    if not code_refs.load_corpus():
        print("[Code] no corpus -> reasoning-only (no citations)")
    batches = _sheet_batches(sheets, config.CODE_BATCH_SIZE)

    issues: List[Dict] = []
    with ThreadPoolExecutor(max_workers=config.CODE_CONCURRENCY) as pool:
        for res in pool.map(lambda b: _review_batch(b, jurisdiction, sheet_index, paths), batches):
            issues.extend(res)
    print(f"[Code] {len(issues)} code/accessibility issue(s) across {len(batches)} batch(es)")
    return issues
