"""Terminal banner + SVG "report card" previews for the abom CLI.

Two kinds of preview, both pure stdlib (no new deps):

  * ``banner()``        — a compact ASCII/Unicode shield + wordmark for the
    terminal, shown at the top of a command's output.
  * ``scan_card()`` / ``gate_card()`` — a self-contained SVG image
    summarizing a scan manifest or a gate decision, written to a file with
    ``--card path.svg`` so it can be opened, screenshotted, or embedded in docs.

The SVG uses ABOM's "Deep Ocean" palette so the cards match the website.
"""
from __future__ import annotations

import sys
from html import escape

# ---- palette (matches the website) -------------------------------------
NAVY = "#0F2A4A"
NAVY2 = "#1C4A6E"
MINT = "#2DD4BF"
ICE = "#D5EAE8"
WHITE = "#FFFFFF"
SLATE = "#9FC6D6"
GREEN = "#1FA37A"
RED = "#E5604D"
AMBER = "#E0A100"


# ---- terminal banner ----------------------------------------------------
# ANSI colors approximating the Deep Ocean palette (256-color, with fallbacks).
_MINT = "\033[38;5;43m"    # teal/mint
_ICE = "\033[38;5;152m"    # ice
_DIM = "\033[38;5;66m"     # slate
_BOLD = "\033[1m"
_RESET = "\033[0m"

# A Unicode shield drawn beside the ABOM wordmark.
_SHIELD = [
    " ╭───────╮ ",
    " │   ✔   │ ",
    " ╰──╮ ╭──╯ ",
    "    ╰─╯    ",
]
_WORDMARK = [
    "▄▀█ █▄▄ █▀█ █▀▄▀█",
    "█▀█ █▄█ █▄█ █░▀░█",
]


def banner(version: str = "", *, color: bool | None = None) -> str:
    """Return the ABOM terminal banner.

    Renders a Unicode shield + wordmark + tagline. Uses ANSI color when stdout
    is a TTY (or ``color=True``); falls back to a clean plain-text banner when
    output is piped/redirected, so it never corrupts logs or CI.
    """
    if color is None:
        try:
            color = sys.stdout.isatty()
        except Exception:
            color = False

    ver = f"v{version}" if version else ""

    tagline = "a signed record of what your AI agent is, what it can do, and what it did"

    if not color:
        # plain, dependency-free, pipe-safe
        line = "  " + "─" * 64
        return (
            f"\n  ◈ ABOM — Agent Bill of Materials  {ver}\n"
            f"{line}\n"
            f"  {tagline}\n"
        )

    # colored: shield + wordmark beside it, then a clear tagline line below
    rows = [
        f"  {_MINT}{_SHIELD[0]}{_RESET}   {_BOLD}{_ICE}{_WORDMARK[0]}{_RESET}",
        f"  {_MINT}{_SHIELD[1]}{_RESET}   {_BOLD}{_ICE}{_WORDMARK[1]}{_RESET}",
        f"  {_MINT}{_SHIELD[2]}{_RESET}   {_DIM}Agent Bill of Materials  {_MINT}{ver}{_RESET}",
        f"  {_MINT}{_SHIELD[3]}{_RESET}",
        f"  {_DIM}{tagline}{_RESET}",
    ]
    return "\n" + "\n".join(rows) + "\n"


# ---- findings table -----------------------------------------------------
_TYPE_LABEL = {
    "model": "model", "tool": "tool", "prompt": "prompt", "dataSource": "data source",
    "policy": "policy", "framework": "framework", "mcpServer": "MCP server",
}
# rough display order so related rows group together
_TYPE_ORDER = ["model", "framework", "tool", "mcpServer", "dataSource", "policy", "prompt"]


def findings_table(components: list[dict], *, color: bool | None = None) -> str:
    """A box-drawn table: one row per detected component (type · name · source).

    Pure stdlib. Colorizes the type column when on a TTY; plain otherwise.
    """
    if color is None:
        try:
            color = sys.stdout.isatty()
        except Exception:
            color = False

    rows = []
    for c in sorted(components, key=lambda c: (_TYPE_ORDER.index(c.get("type"))
                    if c.get("type") in _TYPE_ORDER else 99, c.get("name", ""))):
        rows.append((
            _TYPE_LABEL.get(c.get("type"), c.get("type", "?")),
            str(c.get("name", "?")),
            str(c.get("version", "") or "—"),
            str(c.get("detected_from", "")),
        ))
    if not rows:
        return "  no agent components detected (is this an agent repo?)\n"

    headers = ("#", "TYPE", "NAME", "VERSION", "DETECTED FROM")
    # number the rows
    numbered = [(str(i + 1), t, n, v, d) for i, (t, n, v, d) in enumerate(rows)]

    widths = [max(len(str(cell)) for cell in col) for col in zip(headers, *numbered)]

    def fmt_row(cells, sep="│", pad=" "):
        out = []
        for cell, w in zip(cells, widths):
            out.append(f"{pad}{str(cell):<{w}}{pad}")
        return "  " + sep + sep.join(out) + sep

    def rule(left, mid, right):
        return "  " + left + mid.join("─" * (w + 2) for w in widths) + right

    type_color = {"model": _MINT, "framework": _ICE, "tool": _MINT,
                  "mcpServer": _ICE, "dataSource": _ICE, "policy": _ICE, "prompt": _DIM}

    lines = [rule("┌", "┬", "┐")]
    lines.append(fmt_row(headers))
    lines.append(rule("├", "┼", "┤"))
    for i, (num, t, n, v, d) in enumerate(numbered):
        raw = (num, t, n, v, d)
        if color:
            comp_type = rows[i][0]
            tc = next((col for k, col in type_color.items()
                       if _TYPE_LABEL.get(k) == comp_type), _ICE)
            ver_col = _ICE if v != "—" else _DIM
            cells = []
            colored = (f"{_DIM}{num}{_RESET}", f"{tc}{t}{_RESET}", n,
                       f"{ver_col}{v}{_RESET}", f"{_DIM}{d}{_RESET}")
            # pad based on visible width (raw), then inject color
            for raw_cell, col_cell, w in zip(raw, colored, widths):
                pad_extra = w - len(str(raw_cell))
                cells.append(" " + col_cell + " " * pad_extra + " ")
            lines.append("  │" + "│".join(cells) + "│")
        else:
            lines.append(fmt_row(raw))
    lines.append(rule("└", "┴", "┘"))
    return "\n".join(lines) + "\n"


# ---- shared SVG helpers -------------------------------------------------
def _shield(x: float, y: float, s: float, stroke: str = MINT) -> str:
    """A shield glyph path, scaled to size ``s`` at (x, y)."""
    return (
        f'<g transform="translate({x},{y}) scale({s/100})">'
        f'<path d="M50 8 L88 24 V52 C88 78 71 92 50 100 C29 92 12 78 12 52 V24 Z" '
        f'fill="none" stroke="{stroke}" stroke-width="7" stroke-linejoin="round" stroke-linecap="round"/>'
        f'<path d="M32 50 L46 64 L70 36" fill="none" stroke="{stroke}" stroke-width="7" '
        f'stroke-linecap="round" stroke-linejoin="round"/></g>'
    )


def _text(x, y, s, fill, t, *, weight="400", anchor="start", mono=False) -> str:
    fam = "ui-monospace, Menlo, monospace" if mono else "Segoe UI, Roboto, Arial, sans-serif"
    return (f'<text x="{x}" y="{y}" font-family="{fam}" font-size="{s}" '
            f'font-weight="{weight}" fill="{fill}" text-anchor="{anchor}">{escape(str(t))}</text>')


def _wrap(svg_body: str, w: int, h: int) -> str:
    return (f'<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" '
            f'viewBox="0 0 {w} {h}" role="img">{svg_body}</svg>\n')


# ---- scan report card ---------------------------------------------------
TYPE_LABEL = {
    "model": "models", "tool": "tools", "prompt": "prompts", "dataSource": "data sources",
    "policy": "policies", "framework": "frameworks", "mcpServer": "MCP servers",
}


def scan_card(manifest: dict) -> str:
    """An SVG summary card for a Composition Manifest."""
    agent = manifest.get("agent", {})
    comps = manifest.get("components", [])
    by_type: dict[str, int] = {}
    for c in comps:
        by_type[c.get("type", "?")] = by_type.get(c.get("type", "?"), 0) + 1
    sig = manifest.get("signature", {})
    sha = manifest.get("composition_sha256", "")

    W, H = 720, 420
    b = [f'<rect width="{W}" height="{H}" rx="18" fill="{NAVY}"/>']
    b.append(_shield(40, 34, 56))
    b.append(_text(118, 60, 28, WHITE, "ABOM · Composition Manifest", weight="700"))
    b.append(_text(120, 86, 15, MINT, f'{agent.get("name", "agent")} @ {agent.get("version", "?")}'))
    if agent.get("risk_class"):
        b.append(_text(120, 108, 13, SLATE, f'risk class: {agent["risk_class"]}'))

    # component counts as rows
    y = 150
    b.append(_text(40, y - 14, 13, SLATE, "COMPONENTS DETECTED"))
    for t, n in sorted(by_type.items(), key=lambda kv: -kv[1]):
        b.append(f'<rect x="40" y="{y}" width="640" height="34" rx="8" fill="{NAVY2}"/>')
        b.append(_text(58, y + 22, 15, ICE, TYPE_LABEL.get(t, t)))
        b.append(_text(660, y + 22, 16, MINT, n, weight="700", anchor="end"))
        y += 42
        if y > H - 90:
            break

    # signature + hash footer
    b.append(f'<line x1="40" y1="{H-66}" x2="{W-40}" y2="{H-66}" stroke="{NAVY2}" stroke-width="1"/>')
    if sig:
        b.append(_text(40, H - 40, 13, GREEN, f'✓ signed: {sig.get("alg", "")} · key {sig.get("key_id", "?")}'))
    b.append(_text(40, H - 20, 12, SLATE, f'composition_sha256: {sha[:32]}…', mono=True))
    b.append(_text(W - 40, H - 20, 12, SLATE, "abom.ai", anchor="end"))
    return _wrap("".join(b), W, H)


# ---- gate decision card -------------------------------------------------
def gate_card(decision_dict: dict, tool: str) -> str:
    """An SVG summary card for a single gate ALLOW/DENY decision."""
    allowed = decision_dict.get("verdict") == "allow"
    color = GREEN if allowed else RED
    verdict = "ALLOW" if allowed else "DENY"
    rule = decision_dict.get("rule", "")
    reason = decision_dict.get("reason", "")
    head = decision_dict.get("tree_head", {}) or {}
    sig = decision_dict.get("head_signature", {}) or {}

    W, H = 720, 300
    b = [f'<rect width="{W}" height="{H}" rx="18" fill="{NAVY}"/>']
    b.append(_shield(40, 30, 50))
    b.append(_text(110, 56, 22, WHITE, "ABOM · gate decision", weight="700"))
    b.append(_text(112, 80, 14, SLATE, f"tool: {tool}"))

    # big verdict badge
    b.append(f'<rect x="40" y="110" width="200" height="64" rx="12" fill="{color}"/>')
    b.append(_text(140, 152, 30, NAVY if allowed else WHITE, verdict, weight="700", anchor="middle"))
    b.append(_text(264, 138, 16, ICE, rule, weight="700"))
    # wrap reason to ~58 chars
    r = reason if len(reason) <= 58 else reason[:55] + "…"
    b.append(_text(264, 162, 13, SLATE, r))

    # notarized footer
    b.append(f'<line x1="40" y1="{H-66}" x2="{W-40}" y2="{H-66}" stroke="{NAVY2}" stroke-width="1"/>')
    b.append(_text(40, H - 40, 13, MINT, "◈ notarized in the Merkle transparency log"))
    b.append(_text(40, H - 20, 12, SLATE,
                   f'root {head.get("root_hash", "")[:28]}… · key {sig.get("key_id", "?")}', mono=True))
    b.append(_text(W - 40, H - 20, 12, SLATE, "abom.ai", anchor="end"))
    return _wrap("".join(b), W, H)
