#!/usr/bin/env python3
"""CLI entrypoint for Phase 7 step 1.5 — render a final-report HTML view.

Usage:
  okstra-render-report-views.py <path-to-final-report.data.json|final-report.md>
      [--task-key <task-group/task-id>]
      [--task-type <profile>]
      [--seq <NNN>]
      [--source-report <relative-path>]

Structured ``.data.json`` input (schema 2.0 or 3.0) is rendered directly into
the dedicated task HTML template and always produces an HTML sibling. Markdown
input resolves its data sibling first; schema-v1 and quick reports keep the
legacy conditional Markdown renderer.

Output (idempotent — overwrites):
  - <stem>.html  — single-file self-contained HTML view

This script is the canonical single-reference-point. The Node CLI
(``bin/okstra render-views``) is a thin wrapper that spawns it.
"""
from __future__ import annotations

import argparse
import json
import os
import re
import sys
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[1]
SCRIPTS_DIR = REPO_ROOT / "scripts"
HOME_LIB = (
    Path(os.environ.get("OKSTRA_HOME", str(Path.home() / ".okstra")))
    / "lib"
    / "python"
)

# Prefer dev sources when present, fall back to install. Without this
# order, a stale install (~/.okstra/lib/python/okstra_ctl) without the
# report_views module shadows the in-repo copy during development.
if (SCRIPTS_DIR / "okstra_ctl" / "report_views.py").is_file():
    if str(SCRIPTS_DIR) in sys.path:
        sys.path.remove(str(SCRIPTS_DIR))
    sys.path.insert(0, str(SCRIPTS_DIR))
    if HOME_LIB.is_dir() and str(HOME_LIB) not in sys.path:
        sys.path.append(str(HOME_LIB))
elif HOME_LIB.is_dir() and str(HOME_LIB) not in sys.path:
    sys.path.insert(0, str(HOME_LIB))

from okstra_ctl.clarification_items import STRUCTURED_REPORT_VERSIONS  # noqa: E402
from okstra_ctl.report_views import infer_run_meta, render_html_view  # noqa: E402
from okstra_ctl.final_report_paths import (  # noqa: E402
    final_report_data_path,
    final_report_markdown_path,
)


_OKSTRA_HOME = Path(os.environ.get("OKSTRA_HOME", str(Path.home() / ".okstra")))
# Search order:
#   1) dev tree (`<repo>/templates/reports`) — wins when this script runs from
#      a source checkout or a link-mode install (the symlinked bin entrypoint
#      resolves __file__ back into the repo).
#   2) install tree (`~/.okstra/templates/reports`) — populated by
#      `okstra install` copy mode via src/install.mjs. The legacy
#      `~/.okstra/lib/templates/reports` path was never written by any
#      install flow and is gone.
_TEMPLATES_DIRS = (
    REPO_ROOT / "templates" / "reports",
    _OKSTRA_HOME / "templates" / "reports",
)

def _load_assets() -> tuple[str, str]:
    css_text: str | None = None
    js_text: str | None = None
    for d in _TEMPLATES_DIRS:
        css = d / "report.css"
        js = d / "report.js"
        if css.is_file() and js.is_file():
            css_text = css.read_text(encoding="utf-8")
            js_text = js.read_text(encoding="utf-8")
            break
    if css_text is None or js_text is None:
        raise SystemExit(
            "report.css / report.js not found. Looked under: "
            + ", ".join(str(d) for d in _TEMPLATES_DIRS)
        )
    return css_text, js_text


def _templates_root() -> Path:
    for directory in _TEMPLATES_DIRS:
        if (directory / "html" / "base.template.html").is_file():
            return directory
    raise SystemExit(
        "schema-v2 HTML templates not found. Looked under: "
        + ", ".join(str(directory) for directory in _TEMPLATES_DIRS)
    )


def _report_pair(report_path: Path) -> tuple[Path, Path]:
    if report_path.name.endswith(".data.json"):
        return report_path, final_report_markdown_path(report_path)
    return final_report_data_path(report_path), report_path


def _load_data_if_present(data_path: Path) -> dict | None:
    if not data_path.is_file():
        return None
    try:
        data = json.loads(data_path.read_text(encoding="utf-8"))
    except json.JSONDecodeError as exc:
        raise SystemExit(
            f"final-report data is not valid JSON: {data_path} ({exc})"
        ) from exc
    if not isinstance(data, dict):
        raise SystemExit(f"final-report data must be a JSON object: {data_path}")
    return data


def _v2_run_meta(data: dict, data_path: Path, args: argparse.Namespace):
    """Build the HTML run meta from the data.json and its own filename.

    The markdown sibling contributes nothing but its name, which is derived
    from this path — so a report whose markdown has not been rendered (or was
    deleted) still produces the human view.
    """
    from okstra_ctl.report_html.models import HtmlRunMeta

    header = data.get("header", {})
    task_key = args.task_key or header.get("taskKey")
    task_type = args.task_type or header.get("taskType")
    match = re.search(r"-(\d+)\.data\.json$", data_path.name)
    seq = (
        args.seq
        or data.get("analysisCommon", {}).get("runSeq")
        or (match.group(1) if match else None)
    )
    if not all(
        isinstance(value, str) and value for value in (task_key, task_type, seq)
    ):
        raise SystemExit("schema-v2 report metadata requires task-key, task-type, and seq")
    return HtmlRunMeta(
        task_key,
        task_type,
        seq,
        args.source_report or final_report_markdown_path(data_path).name,
        _elapsed_ms(data_path, task_type, seq),
    )


def _elapsed_ms(report_path, task_type: str, seq: str) -> int | None:
    """Wall-clock milliseconds for this run, or None when it cannot be measured.

    The measurement lives in the run's team-state, not in the report data, so
    the CLI resolves it here rather than teaching the renderer about run
    layout. A missing or timestamp-less state yields None — the header then
    omits the field instead of printing a zero.
    """
    from okstra_ctl.report_view_artifacts import team_state_path_for_report
    from okstra_ctl.time_report import wall_clock_ms

    state_path = team_state_path_for_report(report_path, task_type, seq)
    try:
        state = json.loads(state_path.read_text(encoding="utf-8"))
    except (OSError, ValueError):
        return None
    return wall_clock_ms(state) or None if isinstance(state, dict) else None


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(
        description="Render the self-contained HTML view of an okstra final-report."
    )
    parser.add_argument("report_path", type=Path)
    parser.add_argument("--task-key", default=None)
    parser.add_argument("--task-type", default=None)
    parser.add_argument("--seq", default=None)
    parser.add_argument("--source-report", default=None)
    args = parser.parse_args(argv)

    report_path = args.report_path.resolve()
    if not report_path.is_file():
        parser.error(f"final-report not found: {report_path}")
    data_path, markdown_path = _report_pair(report_path)
    data = _load_data_if_present(data_path)

    if data is not None and data.get("schemaVersion") in STRUCTURED_REPORT_VERSIONS:
        from okstra_ctl.report_html.render import render_v2_html_view

        html_path = render_v2_html_view(
            data_path,
            run_meta=_v2_run_meta(data, data_path, args),
            templates_root=_templates_root(),
        )
        print(f"html: {html_path}")
        return 0

    if not markdown_path.is_file():
        parser.error(f"legacy markdown sibling not found: {markdown_path}")
    meta = infer_run_meta(
        markdown_path,
        task_key=args.task_key,
        task_type=args.task_type,
        seq=args.seq,
        source_report=args.source_report,
    )
    css, js = _load_assets()
    html_path = render_html_view(markdown_path, run_meta=meta, css=css, js=js)
    if html_path is None:
        print(
            "html: skipped (no §1 clarification rows — html view carries no "
            "interactive forms for this report)"
        )
    else:
        print(f"html: {html_path}")
    return 0


if __name__ == "__main__":
    sys.exit(main())
