"""Render schema-v2 data directly into a task-specific HTML document."""
from __future__ import annotations

import hashlib
import json
import sys
from pathlib import Path

import okstra_vendor  # noqa: F401  # registers vendored dependency aliases
from jinja2 import Environment, FileSystemLoader, StrictUndefined, select_autoescape

from ..final_report_paths import final_report_data_path, translation_sidecar_path
from ..final_report_schema import load_schema_for_data, validate
from ..i18n import HTML_DICTIONARY_REL, load_dictionary, make_jinja_global
from ..report_translation import overlay
from ..report_view_artifacts import user_responses_dir_for_report
from ..usage_cells import format_duration_ms
from ..json_boundary import load_owned_object
from .common import anchor_index
from .filters import (
    code_evidence,
    enum_label,
    enum_legend,
    evidence_refs,
    inline_code,
    paragraphs,
)
from .models import HtmlRunMeta
from .report_index import inject_report_index
from .router import HtmlRenderError, resolve_html_route
from .run_usage import run_usage


def _sha256(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def _templates_root(start: Path | None = None) -> Path:
    if start is not None:
        return start
    here = Path(__file__).resolve()
    for parent in [here, *here.parents]:
        candidate = parent / "templates" / "reports"
        if candidate.is_dir():
            return candidate
    raise HtmlRenderError("could not locate templates/reports")


def _elapsed_text(elapsed_ms: int | None) -> str | None:
    """Render a run duration, or nothing when there is none to render.

    A run whose team-state never recorded timestamps has no measured duration;
    printing "0m" would claim it finished instantly. Measured durations go
    through the same formatter as the per-agent cells below, so the header and
    the run-cost table do not spell one quantity two ways.
    """
    if not elapsed_ms or elapsed_ms < 0:
        return None
    return format_duration_ms(elapsed_ms)


def _report_meta(data: dict, run_meta: HtmlRunMeta) -> dict[str, object]:
    return {
        "createdAt": data.get("header", {}).get("createdAt", ""),
        "taskTitle": data.get("frontmatter", {}).get("title", ""),
        "taskKey": run_meta.task_key,
        "elapsed": _elapsed_text(run_meta.elapsed_ms),
    }


def _localize(data: dict, data_path: Path) -> tuple[dict, str]:
    """Apply the translation sidecar for this report's language, if there is one.

    The data.json is the English SSOT and is never rewritten — the sidecar is
    read here and overlaid onto an in-memory copy, so only this HTML document
    speaks the reader's language. A pointer the sidecar left untranslated
    renders in English; that keeps a half-finished translation readable, and
    the counts are printed because a silently half-empty sidecar otherwise
    ships looking finished.
    """
    lang = str((data.get("meta") or {}).get("reportLanguage") or "en")
    if lang == "en":
        return data, lang
    sidecar_file = translation_sidecar_path(data_path, lang)
    if not sidecar_file.is_file():
        sys.stdout.write(
            f"note: no {lang} translation sidecar at {sidecar_file.name}; "
            "rendering the English source\n"
        )
        return data, lang
    payload = load_owned_object(sidecar_file, artifact="translation sidecar")
    strings = payload.get("strings")
    if not isinstance(strings, dict):
        raise HtmlRenderError(f"translation sidecar has no 'strings' object: {sidecar_file}")
    localized, report = overlay(data, strings)
    sys.stdout.write(
        f"translated {report.applied} string(s) into {lang}"
        f" ({len(report.untranslated)} left in English"
        f", {len(report.unresolved)} unresolved)\n"
    )
    return localized, lang


def _html_path(data_path: Path) -> Path:
    suffix = ".data.json"
    if not data_path.name.endswith(suffix):
        raise HtmlRenderError(f"v2 report path must end with {suffix}: {data_path}")
    return data_path.with_name(data_path.name.removesuffix(suffix) + ".html")


def render_v2_html_view(
    data_path: Path,
    *,
    run_meta: HtmlRunMeta,
    templates_root: Path | None = None,
) -> Path:
    """Render the human HTML from the data.json alone.

    The AI-handoff markdown sibling is a second rendering of this same record,
    never an input here: it used to be read for a `source-md-sha256` stamp that
    no reader ever compared, which made a derived artifact a precondition for
    another derived artifact.
    """
    data = load_owned_object(data_path, artifact="final report record")
    errors = validate(data, load_schema_for_data(data))
    if errors:
        raise HtmlRenderError("invalid v2 final-report data: " + "; ".join(errors[:5]))
    # Validate the SSOT, then localize — the sidecar carries presentation and
    # has no say in whether the report is well-formed.
    data, lang = _localize(data, data_path)
    route = resolve_html_route(run_meta.task_type)
    view = route.view_builder(data)
    root = _templates_root(templates_root)
    env = Environment(loader=FileSystemLoader(str(root)), autoescape=select_autoescape(("html",)), undefined=StrictUndefined)
    env.policies["json.dumps_kwargs"] = {"sort_keys": True, "ensure_ascii": False}
    # Binding the index here is what lets a template cite an id without
    # threading the index through every macro and call site.
    anchors = anchor_index(data, view.omitted_fields)
    chrome = load_dictionary(lang, HTML_DICTIONARY_REL)
    translate = make_jinja_global(chrome)
    env.globals["t"] = translate
    env.filters["code_evidence"] = code_evidence
    env.filters["enum_label"] = lambda value, vocabulary: enum_label(value, vocabulary, chrome)
    env.filters["enum_legend"] = lambda vocabulary: enum_legend(vocabulary, chrome)
    env.filters["evidence_refs"] = lambda refs: evidence_refs(refs, anchors)
    env.filters["inline_code"] = lambda value: inline_code(value, anchors)
    env.filters["paragraphs"] = lambda value: paragraphs(value, anchors)
    response_js = (root / "report.js").read_text(encoding="utf-8")
    base_js = (root / "html/assets/base.js").read_text(encoding="utf-8")
    source_data = final_report_data_path(Path(run_meta.source_report)).as_posix()
    context = {
        **view.context,
        "runMeta": run_meta,
        "reportMeta": _report_meta(data, run_meta),
        "taskType": view.task_type,
        "lang": lang,
        "sourceData": source_data,
        "dataSha256": _sha256(data_path),
        "clarificationItems": data.get("clarificationItems", []),
        # 합의·이견 근거는 태스크 본문과 같이 기본 화면에 올린다. 근거 대장
        # 감사 모드에만 두면 판정이 사용자에게 안 보인다.
        "crossVerification": data.get("crossVerification") or {},
        # Every task type ends with the same run-cost section, so it is bound
        # here rather than in ten view models that would each rebuild it.
        "runUsage": run_usage(data),
        "executionRoles": data.get("executionRoles") or [],
        "css": (root / "html/assets/base.css").read_text(encoding="utf-8"),
        "js": response_js + "\n" + base_js,
    }
    output_path = _html_path(data_path)
    document = env.get_template(route.template_name).render(**context)
    output_path.write_text(
        inject_report_index(document, label=translate("base.contents")), encoding="utf-8"
    )
    if context["clarificationItems"] or context.get("directionSelection"):
        # The footer tells the reader to drop the exported file here, so the
        # directory has to exist before they go looking for it.
        user_responses_dir_for_report(data_path).mkdir(parents=True, exist_ok=True)
    return output_path
