"""Forensic report generation — HTML, PDF, and evidence bundles.

Three output formats, auto-detected by file extension:

- **.html** — always available, zero external dependencies
- **.pdf** — requires ``pip install certisigma-census[report]`` (fpdf2)
- **.zip** with ``--bundle`` — self-contained evidence package

All formats share the same section structure, modeled after OSForensics,
Paliscope Build, and the CertiSigma web app's VerificationReport.
"""

from __future__ import annotations

import hashlib
import html
import json
import logging
import zipfile
from dataclasses import asdict
from datetime import datetime, timezone
from io import BytesIO
from typing import Optional

from . import __version__
from .evidence import IntegrityReport, VerifyDetail
from .manifest import Manifest

logger = logging.getLogger(__name__)


def _now_iso() -> str:
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def _sha256_hex(data: bytes) -> str:
    return hashlib.sha256(data).hexdigest()


def _pdf_safe(text: str) -> str:
    """Replace non-latin-1 characters with ASCII equivalents for fpdf2 built-in fonts."""
    return text.encode("latin-1", errors="replace").decode("latin-1")


def _safe_href(url: str) -> str:
    """Validate URL scheme for use in href attributes (defense-in-depth against XSS).

    Only allows https:// URLs. Returns empty string for anything else,
    preventing javascript: or data: scheme injection from compromised API responses.
    """
    if url and url.startswith("https://"):
        return html.escape(url)
    return ""


# ── Data collection ──────────────────────────────────────────────────

def _collect_report_data(
    manifest: Manifest,
    *,
    evidence: Optional[list[VerifyDetail]] = None,
    integrity: Optional[IntegrityReport] = None,
) -> dict:
    """Gather all data needed for report rendering into a plain dict."""
    entries = list(manifest.iter_entries())
    attested = [e for e in entries if e.attested]
    pending = [e for e in entries if not e.attested]

    level_counts: dict[str, int] = {}
    if evidence:
        for d in evidence:
            if d.exists and d.level:
                level_counts[d.level] = level_counts.get(d.level, 0) + 1

    return {
        "generated_at": _now_iso(),
        "tool_version": __version__,
        "manifest_root": manifest.root_dir,
        "total_files": manifest.total,
        "attested_count": len(attested),
        "pending_count": len(pending),
        "level_counts": level_counts,
        "entries": entries,
        "evidence": evidence or [],
        "integrity": integrity,
    }


# ── HTML report ──────────────────────────────────────────────────────

_HTML_TEMPLATE = """\
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CertiSigma Census — Forensic Report</title>
<style>
:root {{ --bg: #fafbfc; --fg: #1a1a2e; --accent: #0f3460; --border: #dde; --ok: #27ae60; --warn: #e74c3c; }}
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{ font-family: "Segoe UI", system-ui, sans-serif; background: var(--bg); color: var(--fg); line-height: 1.6; padding: 2rem; max-width: 1100px; margin: auto; }}
h1 {{ color: var(--accent); border-bottom: 3px solid var(--accent); padding-bottom: .4rem; margin-bottom: 1.5rem; }}
h2 {{ color: var(--accent); margin: 1.5rem 0 .8rem; }}
table {{ width: 100%; border-collapse: collapse; margin: 1rem 0; font-size: .9rem; }}
th {{ background: var(--accent); color: #fff; padding: .5rem .7rem; text-align: left; }}
td {{ padding: .4rem .7rem; border-bottom: 1px solid var(--border); }}
tr:hover td {{ background: #eef; }}
.mono {{ font-family: "Cascadia Code", "Fira Mono", monospace; font-size: .85em; }}
.ok {{ color: var(--ok); font-weight: 600; }}
.warn {{ color: var(--warn); font-weight: 600; }}
.summary {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin: 1rem 0; }}
.card {{ background: #fff; border: 1px solid var(--border); border-radius: 8px; padding: 1rem; }}
.card .label {{ font-size: .85rem; color: #666; }}
.card .value {{ font-size: 1.8rem; font-weight: 700; color: var(--accent); }}
.section {{ margin-bottom: 2rem; }}
footer {{ margin-top: 3rem; border-top: 1px solid var(--border); padding-top: 1rem; font-size: .85rem; color: #888; }}
</style>
</head>
<body>

<h1>CertiSigma Census — Forensic Report</h1>

<div class="section">
<p><strong>Generated:</strong> {generated_at}</p>
<p><strong>Tool version:</strong> certisigma-census {tool_version}</p>
<p><strong>Manifest root:</strong> <span class="mono">{manifest_root}</span></p>
</div>

<h2>Inventory Summary</h2>
<div class="summary">
<div class="card"><div class="label">Total Files</div><div class="value">{total_files}</div></div>
<div class="card"><div class="label">Attested</div><div class="value ok">{attested_count}</div></div>
<div class="card"><div class="label">Pending</div><div class="value">{pending_count}</div></div>
{level_cards}
</div>

<h2>File Inventory</h2>
<table>
<thead><tr><th>Path</th><th>SHA-256</th><th>Size</th><th>Status</th><th>Attestation ID</th></tr></thead>
<tbody>
{file_rows}
</tbody>
</table>

{evidence_section}

{integrity_section}

<h2>Verification Instructions</h2>
<div class="section">
<ol>
<li>Compute the SHA-256 hash of the file: <code>sha256sum &lt;file&gt;</code></li>
<li>Verify via CertiSigma API: <code>census verify &lt;hash&gt;</code> or <code>curl -X POST https://api.certisigma.ch/verify -d '{{"hash_hex":"&lt;hash&gt;"}}'</code></li>
<li>For T2 proofs, verify the OpenTimestamps proof: <code>ots verify &lt;file&gt;.ots</code></li>
<li>Cross-reference the Bitcoin transaction on <a href="https://mempool.space">mempool.space</a></li>
</ol>
</div>

<footer>
<p>Verify this report: <code>sha256sum report.html</code></p>
<p>Generated by CertiSigma Census v{tool_version} — <a href="https://certisigma.ch">certisigma.ch</a></p>
</footer>

</body>
</html>"""


def generate_html_report(
    manifest: Manifest,
    *,
    evidence: Optional[list[VerifyDetail]] = None,
    integrity: Optional[IntegrityReport] = None,
) -> str:
    """Generate a self-contained HTML forensic report. Zero dependencies."""
    data = _collect_report_data(manifest, evidence=evidence, integrity=integrity)

    level_cards = ""
    for level, count in sorted(data["level_counts"].items()):
        level_cards += (
            f'<div class="card"><div class="label">{html.escape(level)}</div>'
            f'<div class="value">{count}</div></div>\n'
        )

    file_rows = ""
    for e in data["entries"]:
        status = '<span class="ok">Attested</span>' if e.attested else "Pending"
        att_id = html.escape(e.attestation_id or "—")
        file_rows += (
            f"<tr><td>{html.escape(e.path)}</td>"
            f'<td class="mono">{html.escape(e.hash_hex[:16])}…</td>'
            f"<td>{e.size:,}</td><td>{status}</td><td>{att_id}</td></tr>\n"
        )

    evidence_section = ""
    if data["evidence"]:
        evidence_section = "<h2>Evidence Chain</h2>\n"
        for d in data["evidence"]:
            if not d.exists:
                continue
            evidence_section += f'<div class="section"><h3>{html.escape(d.hash_hex[:16])}… (ID: {html.escape(d.attestation_id or "?")})</h3>\n'
            evidence_section += f"<p><strong>Level:</strong> {html.escape(d.level or 'unknown')}</p>\n"
            if d.t0:
                algo = html.escape(str(d.t0.get("algorithm", "ECDSA-P256-SHA256")))
                evidence_section += f"<p><strong>T0:</strong> {algo}</p>\n"
            if d.t1:
                tsa = html.escape(str(d.t1.get("tsaProvider", "TSA")))
                ts = html.escape(str(d.t1.get("timestamp", "")))
                evidence_section += f"<p><strong>T1:</strong> {tsa} — {ts}</p>\n"
            if d.t2:
                block = html.escape(str(d.t2.get("bitcoinBlock", "")))
                evidence_section += f"<p><strong>T2:</strong> Bitcoin block {block}</p>\n"
                if d.blockchain_url:
                    safe_url = _safe_href(d.blockchain_url)
                    if safe_url:
                        evidence_section += f'<p>Block: <a href="{safe_url}">{safe_url}</a></p>\n'
                if d.blockchain_tx_url:
                    safe_url = _safe_href(d.blockchain_tx_url)
                    if safe_url:
                        evidence_section += f'<p>Tx: <a href="{safe_url}">{safe_url}</a></p>\n'
            evidence_section += "</div>\n"

    integrity_section = ""
    if data["integrity"]:
        ir = data["integrity"]
        status_class = "warn" if ir.has_discrepancies else "ok"
        status_text = "VIOLATION" if ir.has_discrepancies else "OK"
        integrity_section = f'<h2>Integrity Check</h2>\n<p class="{html.escape(status_class)}">Result: {html.escape(status_text)}</p>\n'
        integrity_section += (
            f"<p>Unchanged: {ir.unchanged} | Modified: {len(ir.modified)} "
            f"| Missing: {len(ir.missing)} | New: {len(ir.new_files)}</p>\n"
        )
        if ir.modified:
            integrity_section += "<h3>Modified Files</h3>\n<table><thead><tr><th>Path</th><th>Expected</th><th>Actual</th><th>Size Change</th></tr></thead><tbody>\n"
            for m in ir.modified:
                integrity_section += (
                    f'<tr><td>{html.escape(m.path)}</td><td class="mono">{html.escape(m.expected_hash[:16])}…</td>'
                    f'<td class="mono">{html.escape(m.actual_hash[:16])}…</td>'
                    f"<td>{m.expected_size:,} → {m.actual_size:,}</td></tr>\n"
                )
            integrity_section += "</tbody></table>\n"
        if ir.missing:
            integrity_section += "<h3>Missing Files</h3>\n<ul>\n"
            for p in ir.missing:
                integrity_section += f"<li>{html.escape(p)}</li>\n"
            integrity_section += "</ul>\n"

    return _HTML_TEMPLATE.format(
        generated_at=html.escape(data["generated_at"]),
        tool_version=html.escape(data["tool_version"]),
        manifest_root=html.escape(data["manifest_root"]),
        total_files=data["total_files"],
        attested_count=data["attested_count"],
        pending_count=data["pending_count"],
        level_cards=level_cards,
        file_rows=file_rows,
        evidence_section=evidence_section,
        integrity_section=integrity_section,
    )


# ── PDF report ───────────────────────────────────────────────────────

def _fpdf2_available() -> bool:
    try:
        import fpdf  # noqa: F401
        return True
    except ImportError:
        return False


def generate_pdf_report(
    manifest: Manifest,
    *,
    evidence: Optional[list[VerifyDetail]] = None,
    integrity: Optional[IntegrityReport] = None,
) -> bytes:
    """Generate a PDF forensic report using fpdf2.

    Raises ImportError if fpdf2 is not installed.
    """
    from fpdf import FPDF

    data = _collect_report_data(manifest, evidence=evidence, integrity=integrity)
    pdf = FPDF()
    pdf.set_auto_page_break(auto=True, margin=20)
    pdf.add_page()

    pdf.set_font("Helvetica", "B", 20)
    pdf.cell(0, 15, "CertiSigma Census", new_x="LMARGIN", new_y="NEXT")
    pdf.set_font("Helvetica", "", 14)
    pdf.cell(0, 10, "Forensic Report", new_x="LMARGIN", new_y="NEXT")
    pdf.ln(5)

    pdf.set_font("Helvetica", "", 10)
    pdf.cell(0, 6, f"Generated: {data['generated_at']}", new_x="LMARGIN", new_y="NEXT")
    pdf.cell(0, 6, f"Tool version: certisigma-census {data['tool_version']}", new_x="LMARGIN", new_y="NEXT")
    pdf.cell(0, 6, _pdf_safe(f"Manifest root: {data['manifest_root']}"), new_x="LMARGIN", new_y="NEXT")
    pdf.ln(8)

    pdf.set_font("Helvetica", "B", 14)
    pdf.cell(0, 10, "Inventory Summary", new_x="LMARGIN", new_y="NEXT")
    pdf.set_font("Helvetica", "", 10)
    pdf.cell(0, 6, f"Total files: {data['total_files']}", new_x="LMARGIN", new_y="NEXT")
    pdf.cell(0, 6, f"Attested: {data['attested_count']}", new_x="LMARGIN", new_y="NEXT")
    pdf.cell(0, 6, f"Pending: {data['pending_count']}", new_x="LMARGIN", new_y="NEXT")
    for level, count in sorted(data["level_counts"].items()):
        pdf.cell(0, 6, _pdf_safe(f"  {level}: {count}"), new_x="LMARGIN", new_y="NEXT")
    pdf.ln(5)

    pdf.set_font("Helvetica", "B", 14)
    pdf.cell(0, 10, "File Inventory", new_x="LMARGIN", new_y="NEXT")
    pdf.set_font("Helvetica", "B", 8)
    col_w = [55, 50, 25, 22, 38]
    headers = ["Path", "SHA-256", "Size", "Status", "Attestation ID"]
    for i, h_text in enumerate(headers):
        pdf.cell(col_w[i], 6, h_text, border=1)
    pdf.ln()

    pdf.set_font("Helvetica", "", 7)
    for e in data["entries"]:
        status = "Attested" if e.attested else "Pending"
        att_id = e.attestation_id or ""
        path_display = e.path if len(e.path) <= 30 else "..." + e.path[-27:]
        hash_display = e.hash_hex[:24] + "..."
        vals = [_pdf_safe(v) for v in [path_display, hash_display, f"{e.size:,}", status, att_id]]
        for i, v in enumerate(vals):
            pdf.cell(col_w[i], 5, v, border=1)
        pdf.ln()

    if data["evidence"]:
        pdf.add_page()
        pdf.set_font("Helvetica", "B", 14)
        pdf.cell(0, 10, "Evidence Chain", new_x="LMARGIN", new_y="NEXT")
        for d in data["evidence"]:
            if not d.exists:
                continue
            pdf.set_font("Helvetica", "B", 9)
            pdf.cell(0, 6, f"Hash: {d.hash_hex[:32]}... (ID: {d.attestation_id or '?'})", new_x="LMARGIN", new_y="NEXT")
            pdf.set_font("Helvetica", "", 8)
            pdf.cell(0, 5, f"Level: {d.level or 'unknown'}", new_x="LMARGIN", new_y="NEXT")
            if d.t0:
                pdf.cell(0, 5, _pdf_safe(f"T0: {d.t0.get('algorithm', 'ECDSA')}"), new_x="LMARGIN", new_y="NEXT")
            if d.t1:
                pdf.cell(0, 5, _pdf_safe(f"T1: {d.t1.get('tsaProvider', 'TSA')} - {d.t1.get('timestamp', '')}"), new_x="LMARGIN", new_y="NEXT")
            if d.t2:
                pdf.cell(0, 5, _pdf_safe(f"T2: Bitcoin block {d.t2.get('bitcoinBlock', '')}"), new_x="LMARGIN", new_y="NEXT")
                if d.blockchain_url:
                    pdf.cell(0, 5, _pdf_safe(f"  Block: {d.blockchain_url}"), new_x="LMARGIN", new_y="NEXT")
                if d.blockchain_tx_url:
                    pdf.cell(0, 5, _pdf_safe(f"  Tx: {d.blockchain_tx_url}"), new_x="LMARGIN", new_y="NEXT")
            pdf.ln(3)

    if data["integrity"]:
        ir = data["integrity"]
        pdf.add_page()
        pdf.set_font("Helvetica", "B", 14)
        pdf.cell(0, 10, "Integrity Check", new_x="LMARGIN", new_y="NEXT")
        pdf.set_font("Helvetica", "", 10)
        status_text = "VIOLATION" if ir.has_discrepancies else "OK"
        pdf.cell(0, 6, f"Result: {status_text}", new_x="LMARGIN", new_y="NEXT")
        pdf.cell(0, 6, f"Unchanged: {ir.unchanged}  Modified: {len(ir.modified)}  Missing: {len(ir.missing)}  New: {len(ir.new_files)}", new_x="LMARGIN", new_y="NEXT")
        if ir.modified:
            pdf.ln(3)
            pdf.set_font("Helvetica", "B", 9)
            pdf.cell(0, 6, "Modified Files:", new_x="LMARGIN", new_y="NEXT")
            pdf.set_font("Helvetica", "", 7)
            for m in ir.modified:
                pdf.cell(0, 5, _pdf_safe(f"  {m.path}  expected: {m.expected_hash[:12]}...  actual: {m.actual_hash[:12]}..."), new_x="LMARGIN", new_y="NEXT")
        if ir.missing:
            pdf.ln(3)
            pdf.set_font("Helvetica", "B", 9)
            pdf.cell(0, 6, "Missing Files:", new_x="LMARGIN", new_y="NEXT")
            pdf.set_font("Helvetica", "", 7)
            for p in ir.missing:
                pdf.cell(0, 5, _pdf_safe(f"  {p}"), new_x="LMARGIN", new_y="NEXT")

    pdf.add_page()
    pdf.set_font("Helvetica", "B", 14)
    pdf.cell(0, 10, "Verification Instructions", new_x="LMARGIN", new_y="NEXT")
    pdf.set_font("Helvetica", "", 9)
    instructions = [
        "1. Compute the SHA-256 hash: sha256sum <file>",
        "2. Verify via CertiSigma: census verify <hash>",
        "3. For T2 proofs: ots verify <file>.ots",
        "4. Cross-reference on mempool.space",
    ]
    for line in instructions:
        pdf.cell(0, 6, line, new_x="LMARGIN", new_y="NEXT")

    pdf.ln(10)
    pdf.set_font("Helvetica", "", 8)
    pdf.cell(0, 5, f"Generated by CertiSigma Census v{data['tool_version']} - certisigma.ch", new_x="LMARGIN", new_y="NEXT")
    pdf.cell(0, 5, "Verify this report: sha256sum report.pdf", new_x="LMARGIN", new_y="NEXT")

    return pdf.output()


# ── Evidence bundle ──────────────────────────────────────────────────

def generate_evidence_bundle(
    manifest: Manifest,
    *,
    client=None,
    evidence: Optional[list[VerifyDetail]] = None,
    integrity: Optional[IntegrityReport] = None,
) -> bytes:
    """Generate a ZIP evidence bundle containing report + proofs.

    Contents:
    - report.html (always)
    - report.pdf (if fpdf2 installed)
    - evidence.json (full T0/T1/T2 data)
    - proofs/*.ots (OpenTimestamps proofs for T2 attestations)
    - SHA256SUMS (checksums of all files in bundle)
    - README.txt (verification instructions)
    """
    buf = BytesIO()
    checksums: dict[str, str] = {}

    with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
        html_content = generate_html_report(
            manifest, evidence=evidence, integrity=integrity,
        )
        html_bytes = html_content.encode("utf-8")
        zf.writestr("report.html", html_bytes)
        checksums["report.html"] = _sha256_hex(html_bytes)

        if _fpdf2_available():
            try:
                pdf_bytes = generate_pdf_report(
                    manifest, evidence=evidence, integrity=integrity,
                )
                zf.writestr("report.pdf", pdf_bytes)
                checksums["report.pdf"] = _sha256_hex(pdf_bytes)
            except Exception as exc:
                logger.warning("PDF generation failed, skipping: %s", exc)

        evidence_data = []
        if evidence:
            for d in evidence:
                evidence_data.append(asdict(d))
        ev_json = json.dumps(evidence_data, indent=2, default=str).encode("utf-8")
        zf.writestr("evidence.json", ev_json)
        checksums["evidence.json"] = _sha256_hex(ev_json)

        if client and evidence:
            import base64

            for d in evidence:
                if d.exists and d.attestation_id and d.t2:
                    try:
                        ev_result = client.get_evidence(d.attestation_id)
                        t2 = ev_result.t2 if hasattr(ev_result, "t2") else ev_result.get("t2")
                        if t2:
                            ots_b64 = t2.get("otsProof") if isinstance(t2, dict) else getattr(t2, "otsProof", None)
                            if ots_b64:
                                ots_bytes = base64.b64decode(ots_b64)
                                safe_name = d.attestation_id.replace("/", "_").replace("\\", "_").replace("..", "_")
                                ots_path = f"proofs/{safe_name}.ots"
                                zf.writestr(ots_path, ots_bytes)
                                checksums[ots_path] = _sha256_hex(ots_bytes)
                    except Exception as exc:
                        logger.warning("Could not save OTS proof for %s: %s", d.attestation_id, exc)

        readme_text = _BUNDLE_README.format(
            generated_at=_now_iso(), tool_version=__version__,
        ).encode("utf-8")
        zf.writestr("README.txt", readme_text)
        checksums["README.txt"] = _sha256_hex(readme_text)

        sums_lines = "".join(
            f"{h}  {name}\n" for name, h in sorted(checksums.items())
        ).encode("utf-8")
        zf.writestr("SHA256SUMS", sums_lines)

    return buf.getvalue()


_BUNDLE_README = """\
CertiSigma Census — Evidence Bundle
====================================

Generated: {generated_at}
Tool version: certisigma-census {tool_version}

This archive contains a self-contained forensic evidence package.

Contents:
  report.html     - Forensic report (open in any browser)
  report.pdf      - Forensic report (if available)
  evidence.json   - Full T0/T1/T2 evidence chain data
  proofs/*.ots    - OpenTimestamps proofs for T2 attestations
  SHA256SUMS      - Checksums of all files in this bundle
  README.txt      - This file

Verification:
  1. Verify file checksums:
     sha256sum -c SHA256SUMS

  2. Verify OpenTimestamps proofs:
     ots verify proofs/<attestation_id>.ots

  3. Verify hashes against CertiSigma:
     census verify <hash>
     # or via API:
     curl -X POST https://api.certisigma.ch/verify -d '{{"hash_hex":"<hash>"}}'

  4. Cross-reference Bitcoin anchors on mempool.space

For more information: https://certisigma.ch
"""
