"""Manifest diffing — compare two manifests to identify changes.

Performs a merge-join on file paths to categorize entries as added,
removed, modified, or unchanged.  Produces structured output suitable
for JSON, CSV, HTML, and terminal rendering.

Exit codes follow the AIDE bitmask convention for scripting:
  0 = no changes
  1 = added files
  2 = removed files
  4 = modified files
  (OR'd together: e.g. 5 = added + modified)
"""

from __future__ import annotations

import html
from dataclasses import dataclass, field
from typing import Optional

from . import __version__
from .manifest import Manifest

EXIT_ADDED = 1
EXIT_REMOVED = 2
EXIT_MODIFIED = 4


@dataclass
class DiffEntry:
    """A single difference between two manifests."""

    path: str
    status: str
    old_hash: Optional[str] = None
    new_hash: Optional[str] = None
    old_size: Optional[int] = None
    new_size: Optional[int] = None


@dataclass
class DiffResult:
    """Result of comparing two manifests."""

    base_root: str = ""
    target_root: str = ""
    added: list[DiffEntry] = field(default_factory=list)
    removed: list[DiffEntry] = field(default_factory=list)
    modified: list[DiffEntry] = field(default_factory=list)
    unchanged_count: int = 0

    @property
    def has_changes(self) -> bool:
        return bool(self.added or self.removed or self.modified)

    @property
    def exit_code(self) -> int:
        """AIDE-style bitmask exit code."""
        code = 0
        if self.added:
            code |= EXIT_ADDED
        if self.removed:
            code |= EXIT_REMOVED
        if self.modified:
            code |= EXIT_MODIFIED
        return code


def diff_manifests(base: Manifest, target: Manifest) -> DiffResult:
    """Compare *base* and *target* manifests by file path.

    Algorithm: build path-indexed dicts from both manifests, then
    merge-join on the union of all paths.  O(n + m) time.
    """
    base_by_path: dict[str, tuple[str, int]] = {}
    for entry in base.iter_entries():
        base_by_path[entry.path] = (entry.hash_hex, entry.size)

    target_by_path: dict[str, tuple[str, int]] = {}
    for entry in target.iter_entries():
        target_by_path[entry.path] = (entry.hash_hex, entry.size)

    result = DiffResult(
        base_root=base.root_dir,
        target_root=target.root_dir,
    )

    all_paths = sorted(set(base_by_path) | set(target_by_path))

    for path in all_paths:
        in_base = base_by_path.get(path)
        in_target = target_by_path.get(path)

        if in_base is None and in_target is not None:
            result.added.append(DiffEntry(
                path=path, status="added",
                new_hash=in_target[0], new_size=in_target[1],
            ))
        elif in_base is not None and in_target is None:
            result.removed.append(DiffEntry(
                path=path, status="removed",
                old_hash=in_base[0], old_size=in_base[1],
            ))
        elif in_base is not None and in_target is not None:
            if in_base[0] != in_target[0]:
                result.modified.append(DiffEntry(
                    path=path, status="modified",
                    old_hash=in_base[0], new_hash=in_target[0],
                    old_size=in_base[1], new_size=in_target[1],
                ))
            else:
                result.unchanged_count += 1

    return result


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

_DIFF_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 — Manifest Diff</title>
<style>
:root {{ --bg: #fafbfc; --fg: #1a1a2e; --accent: #0f3460; --border: #dde; \
--add: #27ae60; --rm: #e74c3c; --mod: #f39c12; }}
* {{ 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; }}
.summary {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 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; }}
.added {{ color: var(--add); }}
.removed {{ color: var(--rm); }}
.modified {{ color: var(--mod); }}
.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 — Manifest Diff</h1>

<div class="section">
<p><strong>Base manifest:</strong> <span class="mono">{base_root}</span></p>
<p><strong>Target manifest:</strong> <span class="mono">{target_root}</span></p>
<p><strong>Generated:</strong> {generated_at}</p>
</div>

<h2>Summary</h2>
<div class="summary">
<div class="card"><div class="label">Added</div>\
<div class="value added">{added_count}</div></div>
<div class="card"><div class="label">Removed</div>\
<div class="value removed">{removed_count}</div></div>
<div class="card"><div class="label">Modified</div>\
<div class="value modified">{modified_count}</div></div>
<div class="card"><div class="label">Unchanged</div>\
<div class="value">{unchanged_count}</div></div>
</div>

{added_section}

{removed_section}

{modified_section}

<footer>
<p>Generated by CertiSigma Census v{tool_version} — \
<a href="https://certisigma.ch">certisigma.ch</a></p>
</footer>

</body>
</html>"""


def generate_diff_html(result: DiffResult) -> str:
    """Generate a self-contained HTML diff report."""
    from datetime import datetime, timezone

    generated_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")

    added_section = ""
    if result.added:
        added_section = '<h2 class="added">Added Files</h2>\n<table>\n'
        added_section += "<thead><tr><th>Path</th><th>SHA-256</th><th>Size</th></tr></thead>\n<tbody>\n"
        for e in result.added:
            h = html.escape(e.new_hash[:16] if e.new_hash else "") + "..."
            size = e.new_size if e.new_size is not None else 0
            added_section += (
                f"<tr><td>{html.escape(e.path)}</td>"
                f'<td class="mono">{h}</td>'
                f"<td>{size:,}</td></tr>\n"
            )
        added_section += "</tbody></table>\n"

    removed_section = ""
    if result.removed:
        removed_section = '<h2 class="removed">Removed Files</h2>\n<table>\n'
        removed_section += "<thead><tr><th>Path</th><th>SHA-256</th><th>Size</th></tr></thead>\n<tbody>\n"
        for e in result.removed:
            h = html.escape(e.old_hash[:16] if e.old_hash else "") + "..."
            size = e.old_size if e.old_size is not None else 0
            removed_section += (
                f"<tr><td>{html.escape(e.path)}</td>"
                f'<td class="mono">{h}</td>'
                f"<td>{size:,}</td></tr>\n"
            )
        removed_section += "</tbody></table>\n"

    modified_section = ""
    if result.modified:
        modified_section = '<h2 class="modified">Modified Files</h2>\n<table>\n'
        modified_section += "<thead><tr><th>Path</th><th>Old Hash</th><th>New Hash</th><th>Size Change</th></tr></thead>\n<tbody>\n"
        for e in result.modified:
            old_h = html.escape(e.old_hash[:16] if e.old_hash else "") + "..."
            new_h = html.escape(e.new_hash[:16] if e.new_hash else "") + "..."
            old_s = e.old_size if e.old_size is not None else 0
            new_s = e.new_size if e.new_size is not None else 0
            modified_section += (
                f"<tr><td>{html.escape(e.path)}</td>"
                f'<td class="mono">{old_h}</td>'
                f'<td class="mono">{new_h}</td>'
                f"<td>{old_s:,} &rarr; {new_s:,}</td></tr>\n"
            )
        modified_section += "</tbody></table>\n"

    return _DIFF_HTML_TEMPLATE.format(
        base_root=html.escape(result.base_root),
        target_root=html.escape(result.target_root),
        generated_at=html.escape(generated_at),
        added_count=len(result.added),
        removed_count=len(result.removed),
        modified_count=len(result.modified),
        unchanged_count=result.unchanged_count,
        added_section=added_section,
        removed_section=removed_section,
        modified_section=modified_section,
        tool_version=html.escape(__version__),
    )
