"""Terminal tables and a self-contained HTML report."""

from __future__ import annotations

import html
import math
from typing import Iterable, Sequence

from .attribution import CaseBlame, ShapleyResult
from .batch import BatchBlame

__all__ = ["format_case", "format_batch", "format_shapley", "render_html", "write_html"]


def _bar(x: float, scale: float, width: int = 20) -> str:
    if scale <= 0 or not math.isfinite(x):
        return ""
    n = int(round(width * min(abs(x), scale) / scale))
    return ("█" if x >= 0 else "░") * n


def _fmt_ci(ci) -> str:
    lo, hi = ci
    if not (math.isfinite(lo) and math.isfinite(hi)):
        return "[   n/a   ]"
    return f"[{lo:+.3f}, {hi:+.3f}]"


def format_shapley(res: ShapleyResult, players: Sequence[str] | None = None) -> str:
    players = list(players or res.values)
    scale = max([abs(v) for v in res.values.values()] + [1e-12])
    lines = [f"{'step':<14}{'Shapley':>9}  {'CI':<18}  ", "-" * 64]
    for p in players:
        ci = _fmt_ci(res.ci[p]) if res.ci else ""
        lines.append(f"{p:<14}{res.values[p]:>+9.3f}  {ci:<18}  {_bar(res.values[p], scale)}")
    lines.append(f"method={res.method}  permutations={res.n_permutations}  "
                 f"coalitions={res.coalitions_evaluated}  cost={res.cost:g}  stop={res.stop_reason}")
    return "\n".join(lines)


def format_case(cb: CaseBlame) -> str:
    sh = cb.shapley
    scale = max([abs(v) for v in sh.values.values()] + [1e-12])
    head = (f"case {cb.case_id}: factual score={cb.factual_score:g}  v(no fix)={cb.v_empty:.3f}  "
            f"v(all fixes)={cb.v_full:.3f}  [{sh.method}, {sh.coalitions_evaluated} coalitions, "
            f"cost {sh.cost:g}]")
    lines = [head, f"{'step':<14}{'fix-alone':>10}{'leave-1-out':>13}{'Shapley':>10}  {'CI':<18}",
             "-" * 78]
    for p in cb.players:
        ci = _fmt_ci(sh.ci[p]) if sh.ci else ""
        lines.append(f"{p:<14}{cb.fix_alone[p]:>+10.3f}{cb.leave_one_out[p]:>+13.3f}"
                     f"{sh.values[p]:>+10.3f}  {ci:<18}  {_bar(sh.values[p], scale, 16)}")
    lines.append(f"last-step-to-touch blames: {cb.last_touch}")
    return "\n".join(lines)


def format_batch(b: BatchBlame, top: int | None = None) -> str:
    lines = [f"pipeline '{b.pipeline}': {b.n_cases} cases run, {b.n_failures} failures, "
             f"{len(b.cases)} analyzed, {b.unresolved} not fixable by any combination; "
             f"cost {b.cost:g}; {b.stop_reason}",
             f"settings: {b.settings}",
             f"{'step':<14}{'fixing alone resolves':>22}  {'mean Shapley [CI]':<28}{'share':>7}"
             f"{'last-touch':>12}", "-" * 98]
    rank = b.ranking()[:top] if top else b.ranking()
    scale = max([abs(s.shapley) for s in b.steps] + [1e-12])
    for s in rank:
        lines.append(f"{s.name:<14}{100 * s.fix_alone:>20.1f} %  {s.shapley:+.3f} {_fmt_ci(s.shapley_ci):<21}"
                     f"{100 * s.share:>6.1f}%{s.last_touch:>12}  {_bar(s.shapley, scale, 14)}")
    return "\n".join(lines)


# ---------------------------------------------------------------------------
# HTML
# ---------------------------------------------------------------------------

_CSS = """
:root{--bg:#fbfaf7;--fg:#1d1d1b;--muted:#6b6a65;--card:#ffffff;--line:#e4e2dc;
--pos:#2f6f9f;--neg:#b5523b;--alt:#7a8f3b;--track:#efede7}
@media (prefers-color-scheme: dark){:root{--bg:#161615;--fg:#ecebe6;--muted:#a3a19a;
--card:#1f1f1d;--line:#34332f;--pos:#6fa8d6;--neg:#e0876f;--alt:#b3c76b;--track:#2a2a27}}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--fg);
font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif}
main{max-width:1000px;margin:0 auto;padding:24px 16px 64px}
h1{font-size:24px;margin:0 0 4px}h2{font-size:19px;margin:36px 0 6px}
.muted{color:var(--muted)}.card{background:var(--card);border:1px solid var(--line);
border-radius:10px;padding:14px 16px;margin:12px 0;overflow-x:auto}
table{border-collapse:collapse;width:100%;font-variant-numeric:tabular-nums}
th,td{text-align:left;padding:5px 8px;border-bottom:1px solid var(--line);vertical-align:middle;white-space:nowrap}
th{font-weight:600;color:var(--muted);font-size:13px}td.num{text-align:right}
.bar{position:relative;height:14px;min-width:160px;background:var(--track);border-radius:3px}
.bar span{position:absolute;top:0;height:14px;border-radius:3px}
.bar .zero{position:absolute;top:-2px;bottom:-2px;width:1px;background:var(--muted)}
.ci{position:absolute;top:6px;height:2px;background:var(--fg);opacity:.55}
details{border-top:1px solid var(--line);padding:6px 0}summary{cursor:pointer}
pre{white-space:pre-wrap;word-break:break-word;font-size:12px;margin:4px 0}
.note{border-left:3px solid var(--pos);padding:2px 10px;margin:8px 0}
"""


def _esc(x) -> str:
    return html.escape(str(x))


def _hbar(x: float, scale: float, ci=None, color="var(--pos)") -> str:
    """Bar on a symmetric [-scale, scale] axis with a zero line and optional CI whisker."""
    if scale <= 0:
        scale = 1.0
    pct = lambda v: 50 + 50 * max(-1.0, min(1.0, v / scale))
    a, b = sorted((pct(0), pct(x)))
    col = color if x >= 0 else "var(--neg)"
    s = f'<div class="bar"><div class="zero" style="left:50%"></div>' \
        f'<span style="left:{a:.2f}%;width:{b - a:.2f}%;background:{col}"></span>'
    if ci and all(math.isfinite(c) for c in ci):
        lo, hi = pct(ci[0]), pct(ci[1])
        s += f'<div class="ci" style="left:{lo:.2f}%;width:{max(hi - lo, 0.3):.2f}%"></div>'
    return s + "</div>"


def _trunc(x, n=240) -> str:
    r = repr(x)
    return r if len(r) <= n else r[:n] + "…"


def _case_html(cb: CaseBlame, open_: bool = False) -> str:
    sh = cb.shapley
    scale = max([abs(v) for v in sh.values.values()] + [abs(v) for v in cb.fix_alone.values()] + [1e-9])
    rows = []
    for p in cb.players:
        ci = sh.ci[p] if sh.ci else None
        rows.append(
            f"<tr><td>{_esc(p)}</td><td class=num>{cb.fix_alone[p]:+.3f}</td>"
            f"<td class=num>{cb.leave_one_out[p]:+.3f}</td><td class=num>{sh.values[p]:+.3f}</td>"
            f"<td>{_hbar(sh.values[p], scale, ci)}</td>"
            f"<td>{'◆' if cb.last_touch == p else ''}</td></tr>")
    coal = ""
    if cb.coalition_values:
        coal = "<p class=muted>Coalition values v(S) (S = set of fixed steps):</p><table>" + "".join(
            f"<tr><td>{_esc(k)}</td><td class=num>{v:.3f}</td></tr>" for k, v in cb.coalition_values.items()
        ) + "</table>"
    outs = "".join(
        f"<tr><td>{_esc(k)}</td><td><pre>{_esc(_trunc(v))}</pre></td>"
        f"<td><pre>{_esc(_trunc(cb.fixed_outputs.get(k)))}</pre></td></tr>"
        for k, v in cb.factual_outputs.items())
    return (f"<details{' open' if open_ else ''}><summary><b>{_esc(cb.case_id)}</b> "
            f"<span class=muted>factual={cb.factual_score:g}, v(∅)={cb.v_empty:.2f}, "
            f"v(all)={cb.v_full:.2f}, top: {_esc(max(sh.values, key=sh.values.get))}</span></summary>"
            f"<table><tr><th>step</th><th>fix alone</th><th>leave-one-out</th><th>Shapley</th>"
            f"<th>Shapley bar (±CI)</th><th>last touch</th></tr>{''.join(rows)}</table>"
            f"<p class=muted>{_esc(sh.method)}; {sh.coalitions_evaluated} coalitions; "
            f"cost {sh.cost:g}; {_esc(sh.stop_reason)}</p>{coal}"
            f"<p class=muted>Outputs: factual vs. all-fixed</p><table><tr><th>value</th>"
            f"<th>factual</th><th>all fixes</th></tr>{outs}</table></details>")


def _batch_html(b: BatchBlame, max_cases: int = 150) -> str:
    scale = max([abs(s.shapley) for s in b.steps] + [abs(s.fix_alone) for s in b.steps] + [1e-9])
    rows = "".join(
        f"<tr><td>{_esc(s.name)}</td><td class=num>{100 * s.fix_alone:.1f}%</td>"
        f"<td>{_hbar(s.fix_alone, scale, s.fix_alone_ci, 'var(--alt)')}</td>"
        f"<td class=num>{s.shapley:+.3f}</td><td>{_hbar(s.shapley, scale, s.shapley_ci)}</td>"
        f"<td class=num>{100 * s.share:.1f}%</td><td class=num>{s.last_touch}</td></tr>"
        for s in b.ranking())
    cases = "".join(_case_html(c) for c in b.cases[:max_cases])
    more = f"<p class=muted>… {len(b.cases) - max_cases} more cases omitted.</p>" if len(b.cases) > max_cases else ""
    return (f"<div class=card><p><b>{b.n_cases}</b> cases run, <b>{b.n_failures}</b> failures, "
            f"<b>{len(b.cases)}</b> analyzed, <b>{b.unresolved}</b> not fixable by any combination of fixes. "
            f"Cost {b.cost:g}. {_esc(b.stop_reason)}.</p><p class=muted>{_esc(b.settings)}</p>"
            f"<table><tr><th>step</th><th colspan=2>fixing this step alone resolves</th>"
            f"<th colspan=2>mean Shapley blame (±CI)</th><th>share</th><th>last-touch</th></tr>{rows}</table>"
            f"</div><div class=card><b>Per-case drill-down</b>{cases}{more}</div>")


def render_html(title: str, sections: Iterable[tuple[str, object]],
                intro: str = "", notes: Sequence[str] = ()) -> str:
    """Build a self-contained HTML page. ``sections`` are ``(heading, obj)``
    with obj a :class:`BatchBlame`, :class:`CaseBlame`, or a plain string (pre-formatted text)."""
    body = []
    for heading, obj in sections:
        body.append(f"<h2>{_esc(heading)}</h2>")
        if isinstance(obj, BatchBlame):
            body.append(_batch_html(obj))
        elif isinstance(obj, CaseBlame):
            body.append(f"<div class=card>{_case_html(obj, open_=True)}</div>")
        else:
            body.append(f"<div class=card><pre>{_esc(obj)}</pre></div>")
    nts = "".join(f"<p class=note>{_esc(n)}</p>" for n in notes)
    return (f"<!doctype html><html lang=en><head><meta charset=utf-8>"
            f"<meta name=viewport content='width=device-width,initial-scale=1'>"
            f"<title>{_esc(title)}</title><style>{_CSS}</style></head><body><main>"
            f"<h1>{_esc(title)}</h1><p class=muted>{_esc(intro)}</p>{nts}{''.join(body)}"
            f"<p class=muted>Legend: <i>fix alone</i> = v({{i}}) − v(∅); <i>leave-one-out</i> = "
            f"v(all) − v(all∖{{i}}); <i>Shapley</i> splits v(all) − v(∅) fairly over all orders of "
            f"applying fixes; ◆ = naive last-step-to-touch blame. Generated by agentblame.</p>"
            f"</main></body></html>")


def write_html(path: str, title: str, sections, intro: str = "", notes: Sequence[str] = ()) -> str:
    with open(path, "w", encoding="utf-8") as f:
        f.write(render_html(title, sections, intro, notes))
    return path
