"""
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

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


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


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

    The panel header shows the day number, topic, and title.  The code is
    syntax-highlighted inside the panel, preceded by the question text.

    Args:
        console:   Rich Console to render into.
        challenge: Challenge dict with keys: topic, title, code_display,
                   question, hints.
        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", "")

    # Detect syntax-highlight language from source; default to Python.
    lang_map = {
        "python": "python",
        "go": "go",
        "java": "java",
        "javascript": "javascript",
        "typescript": "typescript",
        "rust": "rust",
        "c": "c",
        "cpp": "cpp",
        "sql": "sql",
        "kubernetes": "yaml",
        "docker": "dockerfile",
        "terraform": "hcl",
        "cicd": "yaml",
        "bash": "bash",
        "linux": "bash",
    }
    lang = lang_map.get(source.lower(), "python")

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

    syntax = Syntax(code, lang, theme="monokai", line_numbers=False)

    # Print the question above the panel.
    if question:
        console.print()
        console.print(question)
        console.print()

    console.print(Panel(syntax, title=header, title_align="left", expand=False))
    console.print()
    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)
            console.print(f"  {escape(name)}  [dim]{count} matches[/dim]")

    # 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)
    all_matches: list[tuple[float, str, str, str, str]] = []
    for company in companies:
        company_name: str = company.get("name", "")
        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)
            all_matches.append((sim, title, company_name, location, url))

    # 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", "")) 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 in shown:
            parts = [escape(title)]
            if comp:
                parts.append(f"[dim]{escape(comp)}[/dim]")
            if loc:
                parts.append(f"[dim]{escape(loc)}[/dim]")
            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]] = []
    for company in companies:
        company_name: str = company.get("name", "")
        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)
            all_detail_matches.append((sim, title, company_name, location, url))

    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 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]")
            line = " · ".join(parts)
            if url:
                console.print(f"  [link={url}]{line}[/link]")
            else:
                console.print(f"  {line}")
        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()
