#!/usr/bin/env python3
"""Render durable CARBON persona-journey and exploratory-testing reports."""

from __future__ import annotations

import html
import json
import os
import tempfile
from pathlib import Path
from typing import Any

from carbon_brand import REPORT_FAVICON
from carbon_report_visuals import REPORT_VISUAL_CSS, render_decision_hero, render_evidence_spine, render_persona_trace


REPORT_HTML = "persona-journeys.html"
REPORT_MARKDOWN = "persona-journeys.md"
EMOTION_KEYS = ("joy", "happiness", "surprise")
COMPLETED_STATUSES = {"observed", "stopped", "reviewed"}


def _items(value: Any) -> list[dict[str, Any]]:
    return [item for item in value if isinstance(item, dict)] if isinstance(value, list) else []


def _strings(value: Any) -> list[str]:
    return [str(item) for item in value if str(item).strip()] if isinstance(value, list) else []


def _average(feedback: list[dict[str, Any]], key: str) -> float | None:
    values: list[int] = []
    for item in feedback:
        rating = item.get("ratings", {}).get(key) if isinstance(item.get("ratings"), dict) else None
        try:
            score = int(rating)
        except (TypeError, ValueError):
            continue
        if 1 <= score <= 5:
            values.append(score)
    return round(sum(values) / len(values), 1) if values else None


def journey_summary(state: dict[str, Any], manifest: dict[str, Any]) -> dict[str, Any]:
    personas = _items(state.get("personas"))
    feedback = _items(manifest.get("personaFeedback"))
    exploration = _items(manifest.get("explorationObservations"))
    latest_feedback = {str(item.get("personaId")): item for item in feedback if item.get("personaId")}
    completed_feedback = [item for item in latest_feedback.values() if item.get("status") in COMPLETED_STATUSES]
    explored = {str(item.get("personaId")) for item in exploration if item.get("personaId") and item.get("status") not in {"blocked"}}
    missing_inputs = list(dict.fromkeys(
        value
        for item in latest_feedback.values()
        for value in _strings(item.get("missingInputs"))
    ))
    questions = [
        f"To continue persona exploration, please provide or configure: {value}"
        for value in missing_inputs
    ]
    return {
        "schema": "carbon.persona-journeys/v1",
        "plannedPersonas": len(personas),
        "completedJourneys": len({str(item.get("personaId")) for item in completed_feedback if item.get("personaId")}),
        "personasExplored": len(explored),
        "explorationObservations": len(exploration),
        "averageRatings": {key: _average(completed_feedback, key) for key in EMOTION_KEYS},
        "missingInputs": missing_inputs,
        "questionsForUser": questions,
        "htmlReport": str((Path(state.get("_runDir") or ".") / REPORT_HTML)),
        "markdownReport": str((Path(state.get("_runDir") or ".") / REPORT_MARKDOWN)),
    }


def _list_html(values: Any, empty: str = "None recorded") -> str:
    rows = _strings(values)
    return "<ul>" + "".join(f"<li>{html.escape(value)}</li>" for value in rows) + "</ul>" if rows else f'<p class="empty">{html.escape(empty)}</p>'


def _list_md(values: Any, empty: str = "None recorded") -> str:
    rows = _strings(values)
    return "\n".join(f"- {value}" for value in rows) if rows else f"- {empty}"


def _atomic_text(path: Path, value: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
        handle.write(value)
        temporary = Path(handle.name)
    os.replace(temporary, path)


def render_persona_journeys(run_dir: Path, state: dict[str, Any], manifest: dict[str, Any]) -> dict[str, Any]:
    run_dir = run_dir.resolve()
    state_for_summary = {**state, "_runDir": str(run_dir)}
    summary = journey_summary(state_for_summary, manifest)
    personas = _items(state.get("personas"))
    feedback = _items(manifest.get("personaFeedback"))
    exploration = _items(manifest.get("explorationObservations"))
    feedback_by_persona = {str(item.get("personaId")): item for item in feedback if item.get("personaId")}

    cards: list[str] = []
    markdown: list[str] = [
        f"# {state.get('title') or 'CARBON run'} - persona journeys",
        "",
        f"Run: `{state.get('runId', 'unknown')}` | Mode: **{state.get('mode', 'unknown')}** | Status: **{state.get('status', 'unknown')}**",
        "",
        "Persona feedback is qualitative product evidence. Exploratory observations can expose risk and opportunity, but do not by themselves prove deterministic correctness.",
        "",
        "## Summary",
        "",
        f"- Planned personas: {summary['plannedPersonas']}",
        f"- Completed journeys: {summary['completedJourneys']}",
        f"- Personas with exploratory observations: {summary['personasExplored']}",
        f"- Exploratory observations: {summary['explorationObservations']}",
        f"- Average joy: {summary['averageRatings']['joy'] if summary['averageRatings']['joy'] is not None else 'NOT MEASURED'} / 5",
        f"- Average happiness: {summary['averageRatings']['happiness'] if summary['averageRatings']['happiness'] is not None else 'NOT MEASURED'} / 5",
        f"- Average surprise: {summary['averageRatings']['surprise'] if summary['averageRatings']['surprise'] is not None else 'NOT MEASURED'} / 5",
        "",
    ]

    for persona in personas:
        persona_id = str(persona.get("id") or "PERSONA")
        item = feedback_by_persona.get(persona_id, {})
        observations = [row for row in exploration if str(row.get("personaId")) == persona_id]
        ratings = item.get("ratings") if isinstance(item.get("ratings"), dict) else {}
        status = str(item.get("status") or "planned")
        valid_ratings: dict[str, int] = {}
        for key, value in ratings.items():
            try:
                score = int(value)
            except (TypeError, ValueError):
                continue
            if 1 <= score <= 5:
                valid_ratings[str(key)] = score
        rating_html = "".join(
            f'<div><span>{html.escape(key)}</span><progress value="{value}" max="5">{value}/5</progress><b>{value}/5</b></div>'
            for key, value in valid_ratings.items()
        ) or '<p class="empty">Experience ratings are NOT MEASURED.</p>'
        observations_html = "".join(
            f'<article class="observation"><span>{html.escape(str(row.get("strategy") or "explore"))}</span><h4>{html.escape(str(row.get("title") or "Exploratory observation"))}</h4><p>{html.escape(str(row.get("observation") or ""))}</p><small>{html.escape(str(row.get("action") or "Action not recorded"))}</small></article>'
            for row in observations
        ) or '<p class="empty">No exploratory observation recorded for this persona.</p>'
        trace_steps = _items(item.get("experienceTrace"))
        trace_html = render_persona_trace(trace_steps, "Experience through the task")
        cards.append(f'''
        <article class="journey journey-{html.escape(status)}">
          <header><div><span>{html.escape(str(persona.get("role") or "product user"))}</span><h2>{html.escape(str(persona.get("name") or persona_id))}</h2></div><b>{html.escape(status)}</b></header>
          <p><strong>Intent:</strong> {html.escape(str(item.get("intent") or persona.get("intent") or persona.get("goal") or "Complete the scoped product task."))}</p>
          <div class="columns"><section><h3>Plan</h3>{_list_html(item.get("plan") or persona.get("plan"), "Plan not recorded")}</section><section><h3>Attempted actions</h3>{_list_html(item.get("actions"), "Not attempted yet")}</section><section><h3>Outcome</h3><p>{html.escape(str(item.get("outcome") or "Not observed yet"))}</p></section></div>
          <div class="ratings">{rating_html}</div>
          {trace_html}
          <div class="experience"><section><h3>Worked well</h3>{_list_html(item.get("positives"))}</section><section><h3>Joyful moments</h3>{_list_html(item.get("joyfulMoments"))}</section><section><h3>Surprises</h3>{_list_html(item.get("surprises"))}</section><section><h3>Confusion and friction</h3>{_list_html(_strings(item.get("confusions")) + _strings(item.get("frictions")))}</section></div>
          <section><h3>Exploratory observations</h3><div class="observations">{observations_html}</div></section>
          <div class="columns"><section><h3>Recommendations</h3>{_list_html(item.get("recommendations"))}</section><section><h3>Inputs needed</h3>{_list_html(item.get("missingInputs"), "No missing input recorded")}</section><section><h3>Evidence</h3>{_list_html(item.get("evidence"), "No evidence linked yet")}</section></div>
        </article>''')
        markdown.extend([
            f"## {persona.get('name') or persona_id} - {status}", "",
            f"**Role:** {persona.get('role') or 'product user'}", "",
            f"**Intent:** {item.get('intent') or persona.get('intent') or persona.get('goal') or 'Complete the scoped product task.'}", "",
            "### Plan", "", _list_md(item.get("plan") or persona.get("plan"), "Plan not recorded"), "",
            "### Attempted actions", "", _list_md(item.get("actions"), "Not attempted yet"), "",
            f"### Outcome\n\n{item.get('outcome') or 'Not observed yet'}", "",
            "### Experience measures", "",
            *[f"- {key}: {valid_ratings.get(key, 'NOT MEASURED')} / 5" for key in EMOTION_KEYS], "",
            "### Worked well", "", _list_md(item.get("positives")), "",
            "### Joyful moments", "", _list_md(item.get("joyfulMoments")), "",
            "### Surprises", "", _list_md(item.get("surprises")), "",
            "### Confusion and friction", "", _list_md(_strings(item.get("confusions")) + _strings(item.get("frictions"))), "",
            "### Exploratory observations", "",
            *( [f"- **{row.get('title') or 'Observation'}:** {row.get('observation') or ''}" for row in observations] or ["- No exploratory observation recorded for this persona."] ), "",
            "### Recommendations", "", _list_md(item.get("recommendations")), "",
            "### Inputs needed", "", _list_md(item.get("missingInputs"), "No missing input recorded"), "",
        ])

    questions_html = _list_html(summary["questionsForUser"], "No additional user input is currently requested.")
    averages = summary["averageRatings"]
    if summary["plannedPersonas"] and summary["completedJourneys"] == summary["plannedPersonas"] and summary["personasExplored"] == summary["plannedPersonas"]:
        journey_label, journey_tone = "OBSERVED", "neutral"
        journey_headline = "The planned persona journeys produced qualitative product evidence."
    elif summary["missingInputs"]:
        journey_label, journey_tone = "BLOCKED", "risk"
        journey_headline = "The next explorer needs input before the journey can continue."
    else:
        journey_label, journey_tone = "IN PROGRESS", "neutral"
        journey_headline = "Persona evidence is still being gathered."
    narrative_html = render_evidence_spine([
        {"value": "Complete realistic product intents"},
        {"value": f"{summary['plannedPersonas'] - summary['completedJourneys']} journey(s) incomplete", "tone": "risk" if summary['plannedPersonas'] != summary['completedJourneys'] else "neutral"},
        {"value": f"{summary['explorationObservations']} exploratory observation(s)"},
        {"value": journey_label, "tone": "risk" if journey_tone == "risk" else "neutral"},
        {"value": summary["questionsForUser"][0] if summary["questionsForUser"] else "Use the journey evidence in the release decision"},
    ])
    document = f'''<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="color-scheme" content="dark"><link rel="icon" href="{REPORT_FAVICON}"><title>{html.escape(str(state.get("title") or "CARBON"))} - persona journeys</title><style>
    :root{{--bg:#050505;--panel:#0b0b0b;--raised:#111;--line:#303030;--line-bright:#555;--text:#f4f4f2;--muted:#aaa9a4;--muted-strong:#d3d3cf;--good:#f4f4f2;--risk:#ff9f43;--risk-line:rgba(255,159,67,.48)}}*{{box-sizing:border-box}}html{{overflow-x:hidden;-webkit-text-size-adjust:100%;text-size-adjust:100%}}body{{min-height:100vh;min-height:100svh;margin:0;background:var(--bg);color:var(--text);font:15px/1.55 Inter,system-ui,sans-serif}}main{{width:min(1180px,calc(100% - 28px));margin:auto;padding:max(28px,env(safe-area-inset-top)) env(safe-area-inset-right) calc(80px + env(safe-area-inset-bottom)) env(safe-area-inset-left)}}header.top{{display:flex;justify-content:space-between;gap:20px;align-items:center;border-bottom:1px solid var(--line);padding:12px 0 22px}}.brand{{font-weight:900;letter-spacing:.28em}}.hero{{padding:48px 0 28px}}.eyebrow,.journey header span,.observation span{{color:var(--muted);font-size:10px;font-weight:800;letter-spacing:.11em;text-transform:uppercase}}h1{{font-size:clamp(42px,7vw,78px);line-height:.95;letter-spacing:-.06em;margin:10px 0;overflow-wrap:anywhere}}h2,h3,h4{{margin:.2em 0}}.metrics{{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(150px,100%),1fr));gap:1px;margin:18px 0;border:1px solid var(--line);background:var(--line)}}.metric,.input,.journey{{min-width:0;border:1px solid var(--line);background:var(--panel);overflow-wrap:anywhere}}.metric{{padding:14px;border:0}}.metric span{{display:block;color:var(--muted);font-size:10px;text-transform:uppercase}}.metric b{{display:block;font-size:24px;margin-top:5px}}.input{{border-left:3px solid var(--risk);padding:18px;margin:24px 0}}.input h2{{color:var(--risk)}}.journey{{padding:22px;margin:24px 0}}.journey>header{{display:flex;justify-content:space-between;gap:20px;border-bottom:1px solid var(--line);padding-bottom:14px}}.journey>header>b{{align-self:start;border:1px solid var(--line);padding:5px 9px;text-transform:uppercase;font-size:10px}}.journey-observed>header>b{{color:var(--text)}}.journey-blocked>header>b{{color:var(--risk)}}.columns,.experience{{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(220px,100%),1fr));gap:1px;margin:14px 0;border:1px solid var(--line);background:var(--line)}}.columns section,.experience section,.observation{{min-width:0;background:var(--raised);border:0;padding:13px;overflow-wrap:anywhere}}ul{{padding-left:19px}}.empty{{color:var(--muted)}}.ratings{{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(180px,100%),1fr));gap:1px;margin:14px 0;background:var(--line)}}.ratings>div{{display:grid;grid-template-columns:80px minmax(0,1fr) 35px;align-items:center;gap:8px;background:var(--raised);padding:10px}}progress{{width:100%;accent-color:#fff}}.observations{{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(250px,100%),1fr));gap:1px;background:var(--line)}}small{{color:var(--muted)}}a{{color:var(--text);touch-action:manipulation}}@media(max-width:520px){{body{{font-size:16px}}main{{width:min(100% - 20px,1180px);padding-top:max(12px,env(safe-area-inset-top))}}header.top,.journey>header{{align-items:flex-start;flex-direction:column}}header.top>div:last-child{{font-size:12px}}.hero{{padding:32px 0 20px}}h1{{font-size:clamp(38px,13vw,52px)}}.journey{{padding:18px 16px}}.eyebrow,.journey header span,.observation span,.metric span,.journey>header>b{{font-size:11px}}.ratings>div{{grid-template-columns:72px minmax(0,1fr) 30px}}}}
    {REPORT_VISUAL_CSS}
    </style></head><body><main><header class="top"><div class="brand">CARBON</div><div>PERSONA JOURNEYS + EXPLORATORY TESTING</div></header>{render_decision_hero(journey_label, journey_headline, 'Persona feedback is qualitative product evidence. It exposes friction and opportunity but does not prove deterministic correctness.', 'Completed journeys, exploratory observations, missing inputs, and recorded experience measures.', journey_tone)}{narrative_html}<section class="hero"><p class="eyebrow">{html.escape(str(state.get("mode") or "run"))} - {html.escape(str(state.get("status") or "unknown"))}</p><h1>{html.escape(str(state.get("title") or "CARBON run"))}</h1><p>Intent-led product use, emotional response, surprises, confusion, exploratory discoveries, and actionable evidence.</p></section><section class="metrics"><div class="metric"><span>Planned personas</span><b>{summary['plannedPersonas']}</b></div><div class="metric"><span>Completed journeys</span><b>{summary['completedJourneys']}</b></div><div class="metric"><span>Explorers represented</span><b>{summary['personasExplored']}</b></div><div class="metric"><span>Explore observations</span><b>{summary['explorationObservations']}</b></div><div class="metric"><span>Joy</span><b>{averages['joy'] if averages['joy'] is not None else 'NOT MEASURED'}</b></div><div class="metric"><span>Happiness</span><b>{averages['happiness'] if averages['happiness'] is not None else 'NOT MEASURED'}</b></div><div class="metric"><span>Surprise intensity</span><b>{averages['surprise'] if averages['surprise'] is not None else 'NOT MEASURED'}</b></div></section><section class="input"><h2>What the next explorer needs</h2>{questions_html}<p class="empty">Never put passwords, tokens, or private keys in chat. Sign in through the isolated browser or configure named CARBON credential sources.</p></section>{''.join(cards) if cards else '<p class="empty">No personas were planned.</p>'}</main></body></html>'''

    markdown.extend(["## What the next explorer needs", "", _list_md(summary["questionsForUser"], "No additional user input is currently requested."), "", "Never put passwords, tokens, or private keys in chat. Sign in through the isolated browser or configure named CARBON credential sources.", ""])
    html_path = run_dir / REPORT_HTML
    markdown_path = run_dir / REPORT_MARKDOWN
    _atomic_text(html_path, document)
    _atomic_text(markdown_path, "\n".join(markdown).rstrip() + "\n")
    summary["htmlReport"] = str(html_path)
    summary["markdownReport"] = str(markdown_path)
    return summary
