"""
kinlyze/renderer.py
────────────────────────────────────────────────────────────────────────────────
Terminal renderer — zero dependencies, ANSI-aware column alignment.

ANSI-AWARE PADDING:
  Standard Python f-string padding counts invisible escape characters as
  printable width, breaking column alignment when colors are enabled.
  We strip escape codes before measuring string length, then apply padding
  manually. All column-formatted lines go through pad().
────────────────────────────────────────────────────────────────────────────────
"""

import os
import re
import sys
from datetime import datetime, timezone
from typing import Dict, List, Optional

# ── Color state ───────────────────────────────────────────────────────────────

_NO_COLOR = (
    not sys.stdout.isatty()
    or bool(os.environ.get("NO_COLOR"))
    or os.environ.get("TERM") == "dumb"
)

_ANSI_ESCAPE = re.compile(r"\x1b\[[0-9;]*m")


def _c(code: str, text: str) -> str:
    return text if _NO_COLOR else f"\033[{code}m{text}\033[0m"


def bold(t: str)    -> str: return _c("1",  t)
def dim(t: str)     -> str: return _c("2",  t)
def red(t: str)     -> str: return _c("31", t)
def green(t: str)   -> str: return _c("32", t)
def yellow(t: str)  -> str: return _c("33", t)
def cyan(t: str)    -> str: return _c("36", t)
def white(t: str)   -> str: return _c("97", t)
def grey(t: str)    -> str: return _c("90", t)
def bg_red(t: str)  -> str: return _c("41;97", t)


def visible_len(text: str) -> int:
    """Length of string after stripping ANSI escape codes."""
    return len(_ANSI_ESCAPE.sub("", text))


def pad(text: str, width: int, align: str = "left") -> str:
    """
    Pad to `width` printable characters, accounting for invisible ANSI codes.
    align: 'left' | 'right'
    """
    vlen    = visible_len(text)
    padding = max(0, width - vlen) * " "
    return text + padding if align == "left" else padding + text


def risk_color(level: str, text: str) -> str:
    return {
        "critical": red(bold(text)),
        "high":     yellow(text),
        "medium":   yellow(text),
        "low":      green(text),
    }.get(level, text)


RISK_EMOJI  = {"critical": "🔴", "high": "🟠", "medium": "🟡", "low": "🟢"}
TREND_ARROW = {"worsening": red("↓"), "improving": green("↑"), "stable": grey("→"), "new": grey("·")}


# ── Layout ────────────────────────────────────────────────────────────────────

def _tw() -> int:
    try:
        return min(os.get_terminal_size().columns, 110)
    except OSError:
        return 88


def _div(char: str = "─", width: Optional[int] = None) -> str:
    return grey(char * (width or _tw()))


def _section(title: str) -> str:
    return f"\n  {bold(white(title))}\n  {_div()}"


def _bar(pct: float, width: int = 14) -> str:
    filled = round(min(pct, 100) / 100 * width)
    empty  = width - filled
    return cyan("█" * filled) + grey("░" * empty)


def _fmt_date(dt: Optional[datetime]) -> str:
    if dt is None:
        return grey("—")
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=timezone.utc)
    days = (datetime.now(timezone.utc) - dt).days
    if days == 0:   return green("today")
    if days == 1:   return green("yesterday")
    if days <= 7:   return green(f"{days}d ago")
    if days <= 30:  return white(f"{days}d ago")
    if days <= 90:  return yellow(f"{days}d ago")
    return red(f"{days}d ago")


def _fmt_days_inactive(days: int) -> str:
    if days <= 7:   return green(f"{days}d")
    if days <= 30:  return white(f"{days}d")
    if days <= 90:  return yellow(f"{days}d")
    return red(f"{days}d")


# ── Progress ──────────────────────────────────────────────────────────────────

def print_progress(message: str) -> None:
    sys.stdout.write(f"  {grey('›')} {grey(message)}\n")
    sys.stdout.flush()


def print_error(message: str) -> None:
    lines = message.strip().splitlines()
    sys.stderr.write(f"\n  {red('✗')} {bold(red('Error:'))} {lines[0]}\n")
    for line in lines[1:]:
        sys.stderr.write(f"    {line}\n")
    sys.stderr.write("\n")


def print_warning(message: str) -> None:
    sys.stdout.write(f"  {yellow('⚠')} {yellow(message)}\n")


# ── Banner ────────────────────────────────────────────────────────────────────

def _banner() -> None:
    print()
    print(bold(cyan("  ██╗  ██╗██╗███╗   ██╗██╗  ██╗   ██╗███████╗███████╗")))
    print(bold(cyan("  ██║ ██╔╝██║████╗  ██║██║  ╚██╗ ██╔╝╚════██║██╔════╝")))
    print(bold(cyan("  █████╔╝ ██║██╔██╗ ██║██║   ╚████╔╝     ██╔╝█████╗  ")))
    print(bold(cyan("  ██╔═██╗ ██║██║╚██╗██║██║    ╚██╔╝      ██╔╝██╔══╝  ")))
    print(bold(cyan("  ██║  ██╗██║██║ ╚████║███████╗██║        ██║ ███████╗")))
    print(bold(cyan("  ╚═╝  ╚═╝╚═╝╚═╝  ╚═══╝╚══════╝╚═╝        ╚═╝ ╚══════╝")))
    print()
    print(f"  {grey('Analyze the kin behind your code')}  {grey('·')}  {cyan('kinlyze.com')}")


# ── Section 0: Scan summary ───────────────────────────────────────────────────

def _render_summary(data: Dict) -> None:
    info = data.get("repo_info", {})
    s    = data["summary"]
    w    = _tw()

    print(_section("SCAN SUMMARY"))
    print()

    col1 = [
        (grey("Repository"),     bold(cyan(info.get("name", "unknown")))),
        (grey("Branch"),          white(info.get("branch", "—"))),
        (grey("Total commits"),   white(str(info.get("total_commits", "—")))),
        (grey("History window"),  white(f"{data['since_days']} days")),
    ]
    col2 = [
        (grey("Files analyzed"),  white(str(data["files_analyzed"]))),
        (grey("Modules found"),   white(str(data["total_modules"]))),
        (grey("Developers"),      white(str(len(data["developers"])))),
        (grey("Alerts"),          red(bold(str(len(data["alerts"])))) if data["alerts"] else green("0")),
    ]

    for (l1, v1), (l2, v2) in zip(col1, col2):
        print(
            f"  {pad(l1, 20)}  {pad(v1, 26)}"
            f"  {pad(l2, 20)}  {v2}"
        )

    print()
    print(f"  {_div()}")

    # Risk overview bar
    total = sum(s.values()) or 1
    print(
        f"\n  {RISK_EMOJI['critical']}  {pad(red(bold(str(s['critical']))), 4)} critical   "
        f"{RISK_EMOJI['high']}  {pad(yellow(str(s['high'])), 4)} high   "
        f"{RISK_EMOJI['medium']}  {pad(yellow(str(s['medium'])), 4)} medium   "
        f"{RISK_EMOJI['low']}  {pad(green(str(s['low'])), 4)} healthy"
    )

    # Visual proportion bar
    bar_width = min(w - 8, 60)
    critical_w = round(s["critical"] / total * bar_width)
    high_w     = round(s["high"]     / total * bar_width)
    medium_w   = round(s["medium"]   / total * bar_width)
    low_w      = bar_width - critical_w - high_w - medium_w

    if not _NO_COLOR:
        bar_str = (
            "\033[31m" + "█" * critical_w + "\033[0m" +
            "\033[33m" + "█" * high_w     + "\033[0m" +
            "\033[93m" + "█" * medium_w   + "\033[0m" +
            "\033[32m" + "█" * low_w      + "\033[0m"
        )
    else:
        bar_str = "█" * bar_width

    print(f"\n  {bar_str}")
    print()


# ── Section 1: Knowledge Heat Map (Feature 1) ─────────────────────────────────

def _render_heatmap(modules: List[Dict]) -> None:
    if not modules:
        return

    print(_section("KNOWLEDGE HEAT MAP  —  Feature 1"))
    print(f"  {grey('Colour shows concentration risk. Red = one person owns everything.')}")
    print()

    # Column widths
    C_MODULE  = 36
    C_BF      = 4
    C_TREND   = 5
    C_FILES   = 7
    C_COMMITS = 9
    C_OWNER   = 22
    C_PCT     = 8
    C_LAST    = 12

    # Header
    print(
        f"  {pad(grey('MODULE'),    C_MODULE + 3)}"
        f"  {pad(grey('BF'),        C_BF)}"
        f"  {pad(grey('TREND'),     C_TREND)}"
        f"  {pad(grey('FILES'),     C_FILES)}"
        f"  {pad(grey('COMMITS'),   C_COMMITS)}"
        f"  {pad(grey('PRIMARY OWNER'), C_OWNER)}"
        f"  {pad(grey('OWNS'),      C_PCT + 2)}"
        f"  {grey('LAST ACTIVE')}"
    )
    print(f"  {_div()}")

    for m in modules:
        emoji  = RISK_EMOJI.get(m["risk_level"], "⚪")
        module = m["module"]
        if len(module) > C_MODULE - 1:
            module = "…" + module[-(C_MODULE - 2):]

        bf_str    = risk_color(m["risk_level"], str(m["bus_factor"]))
        trend_str = TREND_ARROW.get(m["trend"], grey("·"))
        owner_str = m["primary_owner"][:C_OWNER - 1]
        pct_bar   = _bar(m["primary_pct"], 10)
        pct_str   = cyan(f"{m['primary_pct']}%")
        last_str  = _fmt_date(m.get("last_active"))

        print(
            f"  {emoji} {pad(white(module), C_MODULE)}"
            f"  {pad(bf_str, C_BF)}"
            f"  {pad(trend_str, C_TREND)}"
            f"  {pad(grey(str(m['file_count'])), C_FILES)}"
            f"  {pad(grey(str(m['commit_count'])), C_COMMITS)}"
            f"  {pad(white(owner_str), C_OWNER)}"
            f"  {pct_bar} {pad(pct_str, 7)}"
            f"  {last_str}"
        )

    print()
    print(f"  {grey('BF = Bus Factor  ·  Trend: ')}  {TREND_ARROW['worsening']} {grey('worse')}  {TREND_ARROW['stable']} {grey('stable')}  {TREND_ARROW['improving']} {grey('better')}")
    print()


# ── Section 2: Bus Factor Detail (Feature 2) ──────────────────────────────────

def _render_bus_factor(modules: List[Dict]) -> None:
    if not modules:
        return

    critical = [m for m in modules if m["risk_level"] == "critical"]
    high     = [m for m in modules if m["risk_level"] == "high"]
    risky    = (critical + high)[:10]  # Show top 10 worst

    if not risky:
        print(_section("BUS FACTOR ANALYSIS  —  Feature 2"))
        print(f"\n  {green('✓')} {green('All modules have a healthy bus factor (3+). Great work!')}\n")
        return

    print(_section("BUS FACTOR ANALYSIS  —  Feature 2"))
    print(f"  {grey('Detailed breakdown of critical and high-risk modules.')}")
    print()

    for m in risky:
        emoji    = RISK_EMOJI.get(m["risk_level"], "⚪")
        rl_badge = risk_color(m["risk_level"], f"  {m['risk_level'].upper()}  ")
        bf_badge = risk_color(m["risk_level"], f"BUS FACTOR {m['bus_factor']}")

        print(f"  {emoji}  {bold(white(m['module']))}  {grey('·')}  {bf_badge}")
        print(f"     {_div('·', _tw() - 5)}")

        # Contributors breakdown
        for i, contributor in enumerate(m["contributors"]):
            is_primary = i == 0
            bar_w = round(contributor["pct"] / 100 * 30)
            b = (
                ("\033[31m" if not _NO_COLOR else "") + "█" * bar_w +
                ("\033[0m"  if not _NO_COLOR else "") +
                grey("░" * (30 - bar_w))
            )
            marker = bold(red("►")) if is_primary else grey(" ")
            name   = pad(white(contributor["name"]) if is_primary else grey(contributor["name"]), 24)
            pct    = pad(risk_color(m["risk_level"], f"{contributor['pct']}%") if is_primary else grey(f"{contributor['pct']}%"), 7)

            print(f"     {marker}  {name}  {b}  {pct}")

        # Insight
        if m["bus_factor"] == 1:
            insight = red(f"⚠  If {m['primary_owner']} leaves, nobody else understands this module.")
        else:
            insight = yellow(f"▲  {m['primary_owner']} holds a disproportionate share of knowledge.")

        print(f"\n     {insight}")

        if m["trend"] == "worsening":
            print(f"     {red('↓  Concentration has increased over the last 90 days.')}")
        elif m["trend"] == "improving":
            print(f"     {green('↑  Concentration has decreased over the last 90 days.')}")

        print()


# ── Section 3: Developer Profiles (Feature 3) ────────────────────────────────

def _render_developer_profiles(developers: List[Dict]) -> None:
    if not developers:
        return

    print(_section("DEVELOPER PROFILES  —  Feature 3"))
    print(f"  {grey('Departure impact assessment — how much knowledge each person holds.')}")
    print()

    C_NAME     = 26
    C_PCT      = 8
    C_RISK     = 12
    C_INACTIVE = 10
    C_MODULES  = 8

    print(
        f"  {pad(grey('DEVELOPER'),    C_NAME)}"
        f"  {pad(grey('KNOWLEDGE'),    C_PCT + 2)}"
        f"  {pad(grey('IMPACT'),       C_RISK)}"
        f"  {pad(grey('LAST ACTIVE'),  C_INACTIVE + 2)}"
        f"  {grey('PRIMARY MODULES')}"
    )
    print(f"  {_div()}")

    for dev in developers[:15]:
        name        = dev["name"][:C_NAME - 1]
        pct         = dev["knowledge_pct"]
        risk        = dev["risk"]
        inactive    = dev.get("days_inactive", 0)
        owned       = dev.get("owned_modules", [])
        sole        = dev.get("sole_modules", [])

        pct_bar = _bar(min(pct, 60), 10)   # cap visual at 60% so bar doesn't overflow
        pct_str = pad(risk_color(risk, f"{pct}%"), 7)

        impact_labels = {
            "critical": red(bold("CRITICAL")),
            "high":     yellow("HIGH    "),
            "medium":   yellow("MEDIUM  "),
            "low":      green("LOW     "),
        }
        impact = impact_labels.get(risk, grey("—"))

        last_active_str = _fmt_days_inactive(inactive)

        # Show first 3 owned modules, truncated
        mods_display = ", ".join(m.split("/")[-1] for m in owned[:3])
        if len(owned) > 3:
            mods_display += f" +{len(owned) - 3}"
        if not mods_display:
            mods_display = grey("—")

        print(
            f"  {pad(white(name), C_NAME)}"
            f"  {pct_bar} {pct_str}"
            f"  {pad(impact, C_RISK)}"
            f"  {pad(last_active_str, C_INACTIVE + 2)}"
            f"  {grey(mods_display)}"
        )

        # Sole owner warning
        if sole:
            sole_list = ", ".join(f"'{s}'" for s in sole[:2])
            extra = f" +{len(sole) - 2} more" if len(sole) > 2 else ""
            print(f"  {grey('  └')} {yellow(f'Sole owner of: {sole_list}{extra}')}")

    print()


# ── Section 4: Risk Alerts (Feature 4) ───────────────────────────────────────

def _render_alerts(alerts: List[Dict]) -> None:
    if not alerts:
        print(_section("RISK ALERTS  —  Feature 4"))
        print(f"\n  {green('✓')} {green('No threshold alerts triggered. Repository looks healthy.')}\n")
        return

    print(_section("RISK ALERTS  —  Feature 4"))
    print(f"  {grey(str(len(alerts)))} {grey('alerts require attention.')}")
    print()

    severity_icon = {
        "critical": red("🚨"),
        "high":     yellow("⚠ "),
        "medium":   yellow("● "),
        "low":      grey("· "),
    }

    for i, alert in enumerate(alerts, 1):
        icon     = severity_icon.get(alert["severity"], "  ")
        sev      = risk_color(alert["severity"], alert["severity"].upper())
        title    = bold(white(alert["title"]))
        detail   = alert.get("detail", "")
        action   = alert.get("action", "")

        print(f"  {icon}  [{sev}]  {title}")
        if detail:
            print(f"       {grey(detail)}")
        if action:
            print(f"       {cyan('→')} {action}")
        print()


# ── Main render entry point ───────────────────────────────────────────────────

def render(data: Dict, top: Optional[int] = None) -> None:
    modules    = data["modules"]
    developers = data["developers"]
    alerts     = data["alerts"]

    if top:
        modules = modules[:top]

    _banner()
    print()
    _render_summary(data)
    _render_heatmap(modules)
    _render_bus_factor(modules)
    _render_developer_profiles(developers)
    _render_alerts(alerts)

    # Footer
    print(f"  {_div()}")
    print(f"  {grey('Full dashboard · Slack alerts · GitHub PR integration →')}  {cyan('kinlyze.com')}")
    print()
