"""
Terminal rendering helpers using Rich.

Each function accepts a Rich Console as its first argument so callers can
inject a buffered console in tests without touching stdout.
"""

from __future__ import annotations

import json
import math
from datetime import datetime, timezone

from rich.console import Console
from rich.markdown import Markdown
from rich.markup import escape
from rich.panel import Panel
from rich.syntax import Syntax
from rich.text import Text


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

_BAR_FULL = "█"
_BAR_EMPTY = "░"
_BAR_WIDTH = 16


def _progress_bar(filled: int, total: int, width: int = _BAR_WIDTH) -> str:
    """Return a block-character progress bar string.

    Examples:
        _progress_bar(8, 10, 10) -> "████████░░"
        _progress_bar(0, 10, 10) -> "░░░░░░░░░░"
    """
    if total <= 0:
        return _BAR_EMPTY * width
    ratio = min(filled / total, 1.0)
    filled_count = round(ratio * width)
    return _BAR_FULL * filled_count + _BAR_EMPTY * (width - filled_count)


def _topic_bars(topic_counts: dict[str, int], width: int = _BAR_WIDTH) -> list[tuple[str, str, int]]:
    """Return (topic, bar_str, count) rows scaled to the highest count."""
    if not topic_counts:
        return []
    max_count = max(topic_counts.values())
    rows = []
    for topic, count in sorted(topic_counts.items(), key=lambda kv: -kv[1]):
        bar = _progress_bar(count, max_count, width)
        rows.append((topic, bar, count))
    return rows


def _posted_label(posted_at: str | None) -> str:
    """Convert an ISO timestamp to '~Xd ago', or '' if unparseable."""
    if not posted_at:
        return ""
    try:
        dt = datetime.fromisoformat(posted_at.replace("Z", "+00:00"))
        delta = datetime.now(timezone.utc) - dt
        days = max(0, math.floor(delta.total_seconds() / 86400))
        if days == 0:
            return "~today"
        return f"~{days}d ago"
    except (ValueError, TypeError):
        return ""


# ---------------------------------------------------------------------------
# Public rendering functions
# ---------------------------------------------------------------------------


def render_challenge(console: Console, challenge: dict, *, day: int) -> None:
    """Render a challenge panel to the console.

    Supports two challenge types:
    - predict_output (default): shows code + hint/skip/quit controls
    - multiple_choice: shows code (optional) + A-D choices

    Args:
        console:   Rich Console to render into.
        challenge: Challenge dict with keys: source, title, code_display,
                   question, hints, challenge_type, choices.
        day:       The current challenge day number (shown in the header).
    """
    source: str = challenge.get("source", "")
    title: str = challenge.get("title", "")
    code: str = challenge.get("code_display", "")
    question: str = challenge.get("question", "")
    challenge_type: str = challenge.get("challenge_type", "predict_output")
    choices: list[str] = challenge.get("choices") or []

    # Normalize choices from JSON string if needed.
    if isinstance(choices, str):
        try:
            choices = json.loads(choices)
        except (ValueError, TypeError):
            choices = []

    lang_map = {
        "python": "python",
        "go": "go",
        "java": "java",
        "javascript": "javascript",
        "typescript": "typescript",
        "rust": "rust",
        "c": "c",
        "cpp": "cpp",
        "sql": "sql",
        "spark": "python",
        "pytorch": "python",
        "spring-boot": "java",
        "kubernetes": "yaml",
        "docker": "dockerfile",
        "terraform": "hcl",
        "cicd": "yaml",
        "bash": "bash",
        "linux": "bash",
        "redis": "bash",
        "airflow": "python",
    }
    lang = lang_map.get(source.lower(), "text")

    header = f"Day {day} · {escape(source)} · {escape(title)}"

    console.print()

    # Print code panel if there's code to show, otherwise a header-only panel.
    if code.strip():
        if question:
            console.print(question)
            console.print()
        syntax = Syntax(code, lang, theme="monokai", line_numbers=False)
        console.print(Panel(syntax, title=header, title_align="left", expand=False))
    else:
        # No code — show question inside the panel so header is still visible.
        body = question if question else ""
        console.print(Panel(body, title=header, title_align="left", expand=False))

    console.print()

    if challenge_type == "multiple_choice" and choices:
        for choice in choices:
            console.print(f"  {choice}")
        console.print()
        console.print("[dim]  [A/B/C/D] answer  [h] hint  [s] skip  [q] quit[/dim]")
    else:
        console.print("[dim]\\[h] hint  \\[s] skip  \\[q] quit[/dim]")

    console.print()


def render_result(
    console: Console,
    *,
    correct: bool,
    explanation: str,
    deep_link: str,
    expected_answer: str | None = None,
) -> None:
    """Render the result of a challenge attempt.

    For a correct answer prints a green confirmation.  For an incorrect
    answer prints the expected answer in red, then always shows the
    explanation and a deep-link back to the refresher.

    Args:
        console:         Rich Console to render into.
        correct:         Whether the user's answer was judged correct.
        explanation:     Explanation text shown regardless of correctness.
        deep_link:       URL to the relevant refresher section.
        expected_answer: The correct answer string; shown only on failure.
    """
    console.print()

    if correct:
        console.print("[bold green]✓ Correct![/bold green]")
    else:
        wrong_line = Text("✗ Not quite.")
        if expected_answer is not None:
            wrong_line.append(f"  The answer is: {expected_answer}")
        console.print(wrong_line, style="bold red")

    console.print()
    console.print(explanation)
    console.print()
    console.print(f"[dim]→ Deep dive:[/dim] [cyan]{escape(deep_link)}[/cyan]")
    console.print()


def render_stats(console: Console, stats: dict) -> None:
    """Render a stats summary panel.

    Displays streak, weekly progress, all-time count, per-topic breakdowns,
    and the last session info.

    Args:
        console: Rich Console to render into.
        stats:   Stats dict with keys: streak, total_completed,
                 week_completed, week_goal, topic_counts (dict[str, int]),
                 last_session (dict with date/topic/title keys, or None).
    """
    streak: int = stats.get("streak", 0)
    total: int = stats.get("total_completed", 0)
    week_done: int = stats.get("week_completed", 0)
    week_goal: int = stats.get("week_goal", 10)
    topic_counts: dict[str, int] = stats.get("topic_counts") or {}
    last_session: dict | None = stats.get("last_session")

    streak_icon = " 🔥" if streak > 0 else ""
    console.print()
    console.print(f"[bold]Skilark · {streak} day streak{streak_icon}[/bold]")
    console.print()

    # Weekly progress bar
    week_bar = _progress_bar(week_done, week_goal, 10)
    console.print(
        f"[dim]This week[/dim]        {week_bar}  {week_done}/{week_goal}"
    )
    console.print(
        f"[dim]All time[/dim]         {total} challenges completed"
    )

    # Topic breakdown
    topic_rows = _topic_bars(topic_counts)
    if topic_rows:
        console.print()
        console.print("[bold]Topics[/bold]")
        max_label = max(len(t) for t, _, _ in topic_rows)
        for topic, bar, count in topic_rows:
            padding = " " * (max_label - len(topic))
            console.print(f"{escape(topic)}{padding}  {bar}  {count:>3}")

    # Last session
    if last_session:
        date_str: str = last_session.get("date", "")
        ls_topic: str = last_session.get("topic", "")
        ls_title: str = last_session.get("title", "")
        console.print()
        console.print(
            f"[dim]Last session[/dim]     {escape(date_str)} · {escape(ls_topic)} · {escape(ls_title)}"
        )

    console.print()


def render_hint(console: Console, hint_text: str) -> None:
    """Render a hint in a dimmed style.

    Args:
        console:   Rich Console to render into.
        hint_text: The hint string to display.
    """
    console.print()
    console.print(f"[dim]💡 {escape(hint_text)}[/dim]")
    console.print()


def render_fit_compact(console: Console, data: dict, *, detail: bool = False) -> None:
    """Render a compact summary of find-my-fit results.

    Covers the overview most users need at a glance: match count, salary band,
    top required skills, best-matching companies, and matched position titles.
    For resume-based results, also shows the parsed profile and top skill gaps.

    Args:
        console: Rich Console to render into.
        data:    FindMyFitResponse dict (text or resume variant).
        detail:  When True, skip the truncated positions section (the caller
                 will render the full "All Matches" list instead).
    """
    match_count: int = data.get("match_count", 0)
    companies: list = data.get("companies") or []
    top_skills: list = data.get("top_skills") or []
    salary_range: dict | None = data.get("salary_range")
    common_titles: list = data.get("common_titles") or []
    profile_summary: dict | None = data.get("profile_summary")
    skill_gap: dict | None = data.get("skill_gap")

    console.print()

    if match_count == 0:
        console.print("[dim]No matching positions found.[/dim]")
        console.print()
        return

    company_count = len(companies)
    console.print(
        f"[bold]Found {match_count} matches[/bold] across {company_count} companies"
    )

    # Salary range
    if salary_range:
        lo = int(salary_range.get("min", 0) / 1000)
        hi = int(salary_range.get("max", 0) / 1000)
        med = int(salary_range.get("median", 0) / 1000)
        console.print(
            f"[dim]Salary Range[/dim]    ${lo}K – ${hi}K  [dim](median ${med}K)[/dim]"
        )

    # Top skills (up to 5) — frequency is already a 0-1 float from the API
    if top_skills:
        console.print()
        console.print("[bold]Top Skills[/bold]")
        for entry in top_skills[:5]:
            skill_name: str = entry.get("name", "")
            frequency: float = entry.get("frequency", 0)
            pct = round(frequency * 100)
            console.print(f"  {escape(skill_name)} ({pct}%)")

    # Companies (up to 5)
    if companies:
        console.print()
        console.print("[bold]Companies[/bold]")
        for company in companies[:5]:
            name: str = company.get("name", "")
            count: int = company.get("match_count", 0)
            cp = company.get("closing_period")
            cp_str = f" · [dim]~{cp['median_days_open']}d avg fill[/dim]" if cp else ""
            console.print(f"  {escape(name)}  [dim]{count} matches[/dim]{cp_str}")

    # Matched positions — collect from companies[].matches, then fall back
    # to common_titles when matches list is not present / empty.
    # Tuples: (similarity, title, company_name, location, url, posted_at, is_new, fill_days)
    all_matches: list[tuple[float, str, str, str, str, str, bool, int | None]] = []
    for company in companies:
        company_name: str = company.get("name", "")
        cp = company.get("closing_period")
        fill_days: int | None = cp["median_days_open"] if cp else None
        for match in company.get("matches") or []:
            title: str = match.get("title", "")
            url: str = match.get("url", "")
            location: str = match.get("location", "")
            sim: float = match.get("similarity", 0.0)
            posted_at: str = match.get("posted_at", "")
            is_new: bool = match.get("is_new", False)
            all_matches.append((sim, title, company_name, location, url, posted_at, is_new, fill_days))

    # Sort by similarity descending — most relevant first
    all_matches.sort(key=lambda m: m[0], reverse=True)

    if not all_matches and common_titles:
        all_matches = [(0.0, t.get("title", ""), "", "", t.get("url", ""), "", False, None) for t in common_titles]

    if all_matches and not detail:
        console.print()
        console.print("[bold]Matched Positions[/bold]")
        shown = all_matches[:5]
        for _sim, title, comp, loc, url, posted_at, is_new, fill_days in shown:
            parts = [escape(title)]
            if comp:
                parts.append(f"[dim]{escape(comp)}[/dim]")
            if loc:
                parts.append(f"[dim]{escape(loc)}[/dim]")
            posted = _posted_label(posted_at)
            if posted:
                parts.append(f"[dim]{escape(posted)}[/dim]")
            if fill_days is not None:
                parts.append(f"[dim]~{fill_days}d avg fill[/dim]")
            if is_new:
                parts.append("[bold green]NEW[/bold green]")
            line = " · ".join(parts)
            if url:
                console.print(f"  [link={url}]{line}[/link]")
            else:
                console.print(f"  {line}")
        remaining = len(all_matches) - len(shown)
        if remaining > 0:
            console.print(f"  [dim]+ {remaining} more — use --detail to see all[/dim]")

    # Resume-only: profile summary
    if profile_summary:
        seniority_str: str = profile_summary.get("seniority", "").capitalize()
        years: int = profile_summary.get("years_experience", 0)
        skills: list = profile_summary.get("skills") or []
        role_cat: str = profile_summary.get("role_category", "").replace("_", " ").title()
        console.print()
        console.print("[bold]Your Profile[/bold]")
        console.print(
            f"  {seniority_str} · {role_cat} · {years} yr{'s' if years != 1 else ''}"
        )
        if skills:
            console.print(f"  Skills: {', '.join(skills)}")

    # Resume-only: skill gaps (top 5)
    if skill_gap:
        missing: list = skill_gap.get("missing_skills") or []
        console.print()
        console.print("[bold]Skill Gaps[/bold]")
        for gap_entry in missing[:5]:
            name: str = gap_entry.get("name", "")
            importance: str = gap_entry.get("importance", "")
            console.print(f"  [yellow]![/yellow] {escape(name)}  [dim]{escape(importance)}[/dim]")

    console.print()


def render_coding_problem(console: Console, problem: dict, day: int) -> None:
    """Render a coding problem panel to the console.

    Displays the day number, title, difficulty label (Easy/Medium/Hard),
    category, and description formatted as Markdown inside a Rich Panel.

    Args:
        console: Rich Console to render into.
        problem: Problem dict with keys: title, difficulty (1/2/3), category,
                 description, starter_code_python (optional).
        day:     The current challenge day number (shown in the header).
    """
    _DIFFICULTY_LABELS = {1: "Easy", 2: "Medium", 3: "Hard"}
    _DIFFICULTY_COLORS = {1: "green", 2: "yellow", 3: "red"}

    title: str = problem.get("title", "")
    difficulty: int = problem.get("difficulty", 1)
    category: str = problem.get("category", "")
    description: str = problem.get("description", "")

    label = _DIFFICULTY_LABELS.get(difficulty, "Easy")
    color = _DIFFICULTY_COLORS.get(difficulty, "green")

    header = (
        f"Day {day} · [{color}]{escape(label)}[/{color}]"
        f" · {escape(category)} · {escape(title)}"
    )

    content: list = []
    if description:
        content.append(Markdown(description))

    console.print()
    console.print(Panel(
        *content if content else [""],
        title=header,
        title_align="left",
        expand=False,
    ))
    console.print()


def render_test_results(console: Console, result: dict) -> None:
    """Render per-test-case pass/fail results to the console.

    Shows a checkmark (✓) for passing cases and an X mark (✗) for failing
    ones.  Hidden test case expected values are suppressed.  If an error
    occurred before any cases ran, displays the error message instead.

    Args:
        console: Rich Console to render into.
        result:  Result dict from the execution engine with keys:
                 all_passed (bool), passed_count (int), total_count (int),
                 results (list of per-case dicts), error (str or None).
    """
    error: str | None = result.get("error")
    all_passed: bool = result.get("all_passed", False)
    passed_count: int = result.get("passed_count", 0)
    total_count: int = result.get("total_count", 0)
    cases: list[dict] = result.get("results") or []

    console.print()

    # If there was a top-level error (compile / timeout), show it and return.
    if error and not cases:
        console.print(f"[bold red]Error:[/bold red] {escape(error)}")
        console.print()
        return

    # Per-case results
    for case in cases:
        case_num: int = case.get("case", 0)
        passed: bool = case.get("passed", False)
        expected: str = case.get("expected", "")
        actual: str = case.get("actual", "")
        hidden: bool = case.get("hidden", False)
        time_ms: float = case.get("time_ms", 0.0)

        if passed:
            mark = "[bold green]✓[/bold green]"
            line = f"{mark} Case {case_num + 1}  [dim]{time_ms:.1f}ms[/dim]"
        else:
            mark = "[bold red]✗[/bold red]"
            if hidden:
                # Do not reveal expected value for hidden cases.
                line = f"{mark} Case {case_num + 1}  got: {escape(actual)}  [dim](hidden)[/dim]"
            else:
                line = (
                    f"{mark} Case {case_num + 1}"
                    f"  expected: {escape(expected)}"
                    f"  got: {escape(actual)}"
                    f"  [dim]{time_ms:.1f}ms[/dim]"
                )

        console.print(line)

    # Summary line
    console.print()
    summary_color = "green" if all_passed else "red"
    console.print(
        f"[bold {summary_color}]{passed_count}/{total_count} passed[/bold {summary_color}]"
    )

    # Show any error message that accompanied partial results.
    if error:
        console.print(f"[dim]{escape(error)}[/dim]")

    console.print()


def render_coding_stats(console: Console, stats: dict) -> None:
    """Render a coding problem statistics summary with category progress bars.

    Displays total solved, streak, and a per-category breakdown using the
    same block-character progress bars as render_stats.

    Args:
        console: Rich Console to render into.
        stats:   Stats dict with keys: total_solved (int), streak (int),
                 by_category (dict[str, int] mapping category slug to count).
    """
    total_solved: int = stats.get("total_solved", 0)
    streak: int = stats.get("streak", 0)
    by_category: dict[str, int] = stats.get("by_category") or {}

    streak_icon = " 🔥" if streak > 0 else ""
    console.print()
    console.print(f"[bold]Coding Problems · {streak} day streak{streak_icon}[/bold]")
    console.print()
    console.print(f"[dim]Total solved[/dim]     {total_solved}")

    category_rows = _topic_bars(by_category)
    if category_rows:
        console.print()
        console.print("[bold]By Category[/bold]")
        max_label = max(len(cat) for cat, _, _ in category_rows)
        for cat, bar, count in category_rows:
            padding = " " * (max_label - len(cat))
            console.print(f"{escape(cat)}{padding}  {bar}  {count:>3}")

    console.print()


def render_fit_detail(console: Console, data: dict) -> None:
    """Render a detailed breakdown of find-my-fit results.

    Calls render_fit_compact first for the overview, then adds seniority /
    remote-policy / role-category breakdowns, full per-company job listings
    with URLs, and (for resume results) the complete skill-gap analysis with
    underrepresented skills and a closing recommendation.

    Args:
        console: Rich Console to render into.
        data:    FindMyFitResponse dict (text or resume variant).
    """
    render_fit_compact(console, data, detail=True)

    match_count: int = data.get("match_count", 0)
    if match_count == 0:
        return

    seniority: dict = data.get("seniority") or {}
    remote_policy: dict = data.get("remote_policy") or {}
    role_categories: dict = data.get("role_categories") or {}
    companies: list = data.get("companies") or []
    skill_gap: dict | None = data.get("skill_gap")

    # Seniority breakdown (values are already percentages from the API)
    if seniority:
        console.print("[bold]Seniority[/bold]")
        for level, pct in sorted(seniority.items(), key=lambda kv: -kv[1]):
            console.print(f"  {escape(level)}  {pct}%")
        console.print()

    # Remote policy breakdown (values are already percentages from the API)
    if remote_policy:
        console.print("[bold]Remote Policy[/bold]")
        for policy, pct in sorted(remote_policy.items(), key=lambda kv: -kv[1]):
            console.print(f"  {escape(policy)}  {pct}%")
        console.print()

    # Role category breakdown (values are already percentages from the API)
    if role_categories:
        console.print("[bold]Role Categories[/bold]")
        for category, pct in sorted(role_categories.items(), key=lambda kv: -kv[1]):
            label = category.replace("_", " ").title()
            console.print(f"  {escape(label)}  {pct}%")
        console.print()

    # Full listings — flat list sorted by similarity (most relevant first)
    all_detail_matches: list[tuple[float, str, str, str, str, str, bool, int | None]] = []
    for company in companies:
        company_name: str = company.get("name", "")
        cp = company.get("closing_period")
        fill_days: int | None = cp["median_days_open"] if cp else None
        for match in company.get("matches") or []:
            title: str = match.get("title", "")
            url: str = match.get("url", "")
            location: str = match.get("location", "")
            sim: float = match.get("similarity", 0.0)
            posted_at: str = match.get("posted_at", "")
            is_new: bool = match.get("is_new", False)
            all_detail_matches.append((sim, title, company_name, location, url, posted_at, is_new, fill_days))

    all_detail_matches.sort(key=lambda m: m[0], reverse=True)

    if all_detail_matches:
        console.print("[bold]All Matches[/bold]")
        for _sim, title, comp, loc, url, posted_at, is_new, fill_days in all_detail_matches:
            parts = [escape(title)]
            if comp:
                parts.append(f"[dim]{escape(comp)}[/dim]")
            if loc:
                parts.append(f"[dim]{escape(loc)}[/dim]")
            posted = _posted_label(posted_at)
            if posted:
                parts.append(f"[dim]{escape(posted)}[/dim]")
            if fill_days is not None:
                parts.append(f"[dim]~{fill_days}d avg fill[/dim]")
            if is_new:
                parts.append("[bold green]NEW[/bold green]")
            line = " · ".join(parts)
            if url:
                console.print(f"  [link={url}]{line}[/link]")
            else:
                console.print(f"  {line}")
        console.print()
        console.print("[dim]Posted dates reflect when Skilark first observed the listing.[/dim]")
        console.print()

    # Resume-only: full skill-gap analysis
    if skill_gap:
        missing: list = skill_gap.get("missing_skills") or []
        underrepresented: list = skill_gap.get("underrepresented") or []
        recommendation: str = skill_gap.get("recommendation", "")

        console.print("[bold]Missing Skills[/bold]")
        for gap_entry in missing:
            name: str = gap_entry.get("name", "")
            importance: str = gap_entry.get("importance", "")
            reason: str = gap_entry.get("reason", "")
            console.print(f"  [yellow]![/yellow] {escape(name)}  [dim]{escape(importance)}[/dim]")
            if reason:
                console.print(f"    [dim]{escape(reason)}[/dim]")

        if underrepresented:
            console.print()
            console.print("[bold]Underrepresented Skills[/bold]")
            for entry in underrepresented:
                name: str = entry.get("name", "")
                reason: str = entry.get("reason", "")
                if reason:
                    console.print(f"  {escape(name)}  [dim]{escape(reason)}[/dim]")
                else:
                    console.print(f"  {escape(name)}")

        if recommendation:
            console.print()
            console.print("[bold]Recommendation[/bold]")
            console.print(f"  {escape(recommendation)}")

        console.print()
