"""Colored terminal output for plan results."""

from __future__ import annotations

import csv
import io
import json
import sys
from html import escape as html_escape
from typing import IO

from octorules.phases import PHASE_BY_CF, PHASE_BY_NAME
from octorules.planner import ChangeType, PhasePlan, RuleChange, ZonePlan

# ANSI color codes
GREEN = "\033[32m"
RED = "\033[31m"
YELLOW = "\033[33m"
CYAN = "\033[36m"
BOLD = "\033[1m"
DIM = "\033[2m"
RESET = "\033[0m"


def _supports_color() -> bool:
    """Check if the terminal supports color."""
    return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()


def _color(text: str, code: str, use_color: bool = True) -> str:
    if not use_color:
        return text
    return f"{code}{text}{RESET}"


def _change_symbol(change_type: ChangeType) -> str:
    return {
        ChangeType.ADD: "+",
        ChangeType.REMOVE: "-",
        ChangeType.MODIFY: "~",
        ChangeType.REORDER: "↕",
    }[change_type]


def _change_color(change_type: ChangeType) -> str:
    return {
        ChangeType.ADD: GREEN,
        ChangeType.REMOVE: RED,
        ChangeType.MODIFY: YELLOW,
        ChangeType.REORDER: CYAN,
    }[change_type]


def _compute_field_diffs(change: RuleChange) -> list[tuple[str, object, object]]:
    """Compute field-level diffs between normalized current and desired rules.

    Returns list of (key, old_value, new_value) tuples for changed fields.
    """
    old = change.normalized_current
    new = change.normalized_desired
    if not old or not new:
        return []
    diffs = []
    for key in sorted(old.keys() | new.keys()):
        old_val = old.get(key)
        new_val = new.get(key)
        if old_val != new_val:
            diffs.append((key, old_val, new_val))
    return diffs


def _format_modify_details(change: RuleChange, use_color: bool) -> list[str]:
    """Format field-level diffs for a MODIFY change."""
    details = []
    for key, old_val, new_val in _compute_field_diffs(change):
        details.append(
            _color(f"      {key}: ", DIM, use_color)
            + _color(f"{old_val!r}", RED, use_color)
            + " → "
            + _color(f"{new_val!r}", GREEN, use_color)
        )
    return details


def format_change(change: RuleChange, use_color: bool = True) -> list[str]:
    """Format a single rule change as lines of output."""
    symbol = _change_symbol(change.change_type)
    color = _change_color(change.change_type)
    label = change.change_type.value

    if change.change_type == ChangeType.REORDER:
        return [_color(f"  {symbol} reorder rules", color, use_color)]

    lines = [_color(f"  {symbol} {label}: {change.ref}", color, use_color)]
    if change.change_type == ChangeType.MODIFY:
        lines.extend(_format_modify_details(change, use_color))
    return lines


def format_phase_plan(phase_plan: PhasePlan, use_color: bool = True) -> list[str]:
    """Format a phase plan as lines of output."""
    lines = []
    header = f"  {phase_plan.phase.friendly_name} ({phase_plan.phase.cf_phase})"
    lines.append(_color(header, BOLD, use_color))
    for change in phase_plan.changes:
        lines.extend(format_change(change, use_color))
    return lines


def format_zone_plan(zone_plan: ZonePlan, use_color: bool = True) -> str:
    """Format a full zone plan as a string."""
    lines: list[str] = []

    if not zone_plan.has_changes:
        lines.append(
            _color(f"Zone {zone_plan.zone_name}: ", BOLD, use_color)
            + _color("no changes", DIM, use_color)
        )
        return "\n".join(lines)

    lines.append(
        _color(f"Zone {zone_plan.zone_name}: ", BOLD, use_color)
        + f"{zone_plan.total_changes} change(s)"
    )

    for phase_plan in zone_plan.phase_plans:
        lines.extend(format_phase_plan(phase_plan, use_color))

    return "\n".join(lines)


def _total_changes(zone_plans: list[ZonePlan]) -> int:
    """Sum total changes across all zone plans."""
    return sum(zp.total_changes for zp in zone_plans)


def format_plan_json(zone_plans: list[ZonePlan]) -> str:
    """Format the plan as structured JSON."""
    total_changes = _total_changes(zone_plans)
    zones = []
    for zp in zone_plans:
        phase_plans = []
        for pp in zp.phase_plans:
            changes = []
            for c in pp.changes:
                change_data: dict = {
                    "type": c.change_type.value,
                    "ref": c.ref,
                }
                norm_current = c.normalized_current
                if norm_current is not None:
                    change_data["current"] = norm_current
                norm_desired = c.normalized_desired
                if norm_desired is not None:
                    change_data["desired"] = norm_desired
                changes.append(change_data)
            phase_plans.append(
                {
                    "phase": pp.phase.friendly_name,
                    "cf_phase": pp.phase.cf_phase,
                    "changes": changes,
                }
            )
        zones.append(
            {
                "zone": zp.zone_name,
                "phase_plans": phase_plans,
                "total_changes": zp.total_changes,
                "has_changes": zp.has_changes,
            }
        )
    result = {
        "zones": zones,
        "total_changes": total_changes,
        "has_changes": total_changes > 0,
    }
    return json.dumps(result, indent=2)


def _md_escape(text: str) -> str:
    """Escape pipe characters for markdown tables."""
    return str(text).replace("|", "\\|")


def format_plan_markdown(zone_plans: list[ZonePlan]) -> str:
    """Format the plan as markdown for PR comments."""
    total_changes = _total_changes(zone_plans)
    lines: list[str] = []

    for zp in zone_plans:
        lines.append(f"### Zone: `{zp.zone_name}`")
        lines.append("")
        if not zp.has_changes:
            lines.append("No changes.")
            lines.append("")
            continue
        lines.append("| Op | Phase | Ref | Details |")
        lines.append("|---|---|---|---|")
        for pp in zp.phase_plans:
            for c in pp.changes:
                op = _change_symbol(c.change_type)
                phase = pp.phase.friendly_name
                ref = _md_escape(c.ref)
                if c.change_type == ChangeType.MODIFY and c.current and c.desired:
                    diffs = [
                        f"`{key}`: {old_val!r} → {new_val!r}"
                        for key, old_val, new_val in _compute_field_diffs(c)
                    ]
                    details = _md_escape("; ".join(diffs))
                elif c.change_type == ChangeType.REORDER:
                    details = "reorder rules"
                else:
                    details = ""
                lines.append(f"| {op} | {phase} | {ref} | {details} |")
        lines.append("")

    if total_changes == 0:
        lines.append("**No changes detected.**")
    else:
        lines.append(f"**Total: {total_changes} change(s) across {len(zone_plans)} zone(s).**")
    return "\n".join(lines)


def format_plan_html(zone_plans: list[ZonePlan]) -> str:
    """Format the plan as a self-contained HTML document with inline CSS."""
    total_changes = _total_changes(zone_plans)
    e = html_escape

    css = """\
    body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
           max-width: 960px; margin: 2em auto; padding: 0 1em; color: #24292f; }
    h1 { border-bottom: 1px solid #d1d9e0; padding-bottom: 0.3em; }
    h2 { margin-top: 1.5em; }
    h3 { color: #57606a; }
    table { border-collapse: collapse; width: 100%; margin-bottom: 1em; }
    th, td { border: 1px solid #d1d9e0; padding: 6px 12px; text-align: left; }
    th { background: #f6f8fa; }
    .summary { background: #f6f8fa; padding: 1em; border-radius: 6px; margin-bottom: 1.5em; }
    .add { color: #1a7f37; }
    .remove { color: #cf222e; }
    .modify { color: #9a6700; }
    .reorder { color: #0969da; }
    .diff-old { color: #cf222e; text-decoration: line-through; }
    .diff-new { color: #1a7f37; }
    .no-changes { color: #57606a; font-style: italic; }"""

    lines = [
        "<!DOCTYPE html>",
        "<html>",
        "<head>",
        '<meta charset="utf-8">',
        "<title>octorules plan</title>",
        f"<style>{css}</style>",
        "</head>",
        "<body>",
        "<h1>octorules plan</h1>",
    ]

    # Summary section
    lines.append('<div class="summary">')
    if total_changes == 0:
        lines.append("<p>No changes detected.</p>")
    else:
        lines.append(
            f"<p><strong>{total_changes} change(s)</strong> across {len(zone_plans)} zone(s).</p>"
        )
    lines.append("</div>")

    # Per-zone sections
    for zp in zone_plans:
        lines.append("<section>")
        lines.append(f"<h2>{e(zp.zone_name)}</h2>")

        if not zp.has_changes:
            lines.append('<p class="no-changes">No changes.</p>')
            lines.append("</section>")
            continue

        for pp in zp.phase_plans:
            lines.append(f"<h3>{e(pp.phase.friendly_name)} ({e(pp.phase.cf_phase)})</h3>")
            lines.append("<table>")
            lines.append("<tr><th>Op</th><th>Ref</th><th>Details</th></tr>")

            for c in pp.changes:
                symbol = _change_symbol(c.change_type)
                css_class = c.change_type.value

                if c.change_type == ChangeType.REORDER:
                    lines.append(
                        f'<tr class="{css_class}">'
                        f"<td>{e(symbol)}</td>"
                        f"<td>{e(c.ref)}</td>"
                        f"<td>reorder rules</td></tr>"
                    )
                elif c.change_type == ChangeType.MODIFY and c.current and c.desired:
                    diffs = [
                        f"<code>{e(key)}</code>: "
                        f'<span class="diff-old">{e(repr(old_val))}</span> → '
                        f'<span class="diff-new">{e(repr(new_val))}</span>'
                        for key, old_val, new_val in _compute_field_diffs(c)
                    ]
                    details = "<br>".join(diffs)
                    lines.append(
                        f'<tr class="{css_class}">'
                        f"<td>{e(symbol)}</td>"
                        f"<td>{e(c.ref)}</td>"
                        f"<td>{details}</td></tr>"
                    )
                else:
                    lines.append(
                        f'<tr class="{css_class}">'
                        f"<td>{e(symbol)}</td>"
                        f"<td>{e(c.ref)}</td>"
                        f"<td></td></tr>"
                    )

            lines.append("</table>")

        lines.append("</section>")

    lines.extend(["</body>", "</html>"])
    return "\n".join(lines)


_FORMAT_RENDERERS: dict[str, callable] = {
    "json": format_plan_json,
    "markdown": format_plan_markdown,
    "html": format_plan_html,
}


def print_plan(zone_plans: list[ZonePlan], file: IO[str] | None = None, fmt: str = "text") -> None:
    """Print the full plan for all zones."""
    if file is None:
        file = sys.stdout

    renderer = _FORMAT_RENDERERS.get(fmt)
    if renderer is not None:
        print(renderer(zone_plans), file=file)
        return

    use_color = _supports_color() and file is sys.stdout
    total_changes = _total_changes(zone_plans)

    for zp in zone_plans:
        print(format_zone_plan(zp, use_color), file=file)
        print(file=file)

    if total_changes == 0:
        print(_color("No changes detected.", DIM, use_color), file=file)
    else:
        summary = f"Total: {total_changes} change(s) across {len(zone_plans)} zone(s)."
        print(_color(summary, BOLD, use_color), file=file)


def build_report_data(
    zone_plans: list[ZonePlan],
    desired_by_zone: dict[str, dict],
    current_by_zone: dict[str, dict],
) -> dict:
    """Build structured drift report data across all zones and phases.

    Enumerates all phases present in either desired or current (not just changed
    phases from ZonePlan), so in_sync phases are included in the report.
    """
    # Index phase plans by (zone, cf_phase) for quick lookup
    changes_index: dict[tuple[str, str], PhasePlan] = {}
    for zp in zone_plans:
        for pp in zp.phase_plans:
            changes_index[(zp.zone_name, pp.phase.cf_phase)] = pp

    zones_data = []
    summary_in_sync = 0
    summary_drifted = 0

    for zp in zone_plans:
        zone_name = zp.zone_name
        desired = desired_by_zone.get(zone_name, {})
        current = current_by_zone.get(zone_name, {})

        # Build the union of all phases present in either desired or current
        all_cf_phases: set[str] = set()
        for friendly_name in desired:
            if friendly_name in PHASE_BY_NAME:
                all_cf_phases.add(PHASE_BY_NAME[friendly_name].cf_phase)
        for cf_phase in current:
            if cf_phase in PHASE_BY_CF:
                all_cf_phases.add(cf_phase)

        phases_data = []
        zone_has_drift = False

        for cf_phase in sorted(all_cf_phases):
            if cf_phase not in PHASE_BY_CF:
                continue
            phase = PHASE_BY_CF[cf_phase]
            friendly_name = phase.friendly_name

            yaml_rules = len(desired.get(friendly_name, []))
            live_rules = len(current.get(cf_phase, []))

            # Count changes from the phase plan if one exists
            pp = changes_index.get((zone_name, cf_phase))
            adds = removes = modifies = 0
            if pp:
                for c in pp.changes:
                    if c.change_type == ChangeType.ADD:
                        adds += 1
                    elif c.change_type == ChangeType.REMOVE:
                        removes += 1
                    elif c.change_type == ChangeType.MODIFY:
                        modifies += 1

            # Determine status
            has_yaml = friendly_name in desired
            has_live = cf_phase in current and len(current[cf_phase]) > 0

            if has_yaml and not has_live and yaml_rules > 0:
                status = "yaml_only"
            elif has_live and not has_yaml:
                status = "live_only"
            elif adds > 0 or removes > 0 or modifies > 0:
                status = "drifted"
            else:
                status = "in_sync"

            if status != "in_sync":
                zone_has_drift = True

            phases_data.append(
                {
                    "phase": friendly_name,
                    "cf_phase": cf_phase,
                    "status": status,
                    "yaml_rules": yaml_rules,
                    "live_rules": live_rules,
                    "adds": adds,
                    "removes": removes,
                    "modifies": modifies,
                }
            )

        zone_status = "drifted" if zone_has_drift else "in_sync"
        if zone_status == "in_sync":
            summary_in_sync += 1
        else:
            summary_drifted += 1

        zones_data.append(
            {
                "zone": zone_name,
                "status": zone_status,
                "phases": phases_data,
            }
        )

    return {
        "zones": zones_data,
        "summary": {
            "total_zones": len(zone_plans),
            "in_sync": summary_in_sync,
            "drifted": summary_drifted,
        },
    }


def format_report_csv(report_data: dict) -> str:
    """Format report data as CSV with header, data rows, and summary comment."""
    buf = io.StringIO()
    writer = csv.writer(buf)
    header = [
        "Zone",
        "Phase",
        "CF Phase",
        "Status",
        "YAML Rules",
        "Live Rules",
        "Adds",
        "Removes",
        "Modifies",
    ]
    writer.writerow(header)
    for zone in report_data["zones"]:
        for phase in zone["phases"]:
            writer.writerow(
                [
                    zone["zone"],
                    phase["phase"],
                    phase["cf_phase"],
                    phase["status"],
                    phase["yaml_rules"],
                    phase["live_rules"],
                    phase["adds"],
                    phase["removes"],
                    phase["modifies"],
                ]
            )
    s = report_data["summary"]
    buf.write(
        f"# Summary: {s['total_zones']} zones, {s['in_sync']} in_sync, {s['drifted']} drifted\n"
    )
    return buf.getvalue()


def format_report_json(report_data: dict) -> str:
    """Format report data as pretty-printed JSON."""
    return json.dumps(report_data, indent=2)


def print_report(report_data: dict, file: IO[str] | None = None, fmt: str = "csv") -> None:
    """Print the drift report in the requested format."""
    if file is None:
        file = sys.stdout

    if fmt == "json":
        print(format_report_json(report_data), file=file)
    else:
        print(format_report_csv(report_data), end="", file=file)
