"""
today command — the core challenge loop.

Entry point: `skilark` or `skilark today`

Fetches the next unseen challenge, prompts for an answer, checks it,
records the result, and offers another round.  The interactive loop is
extracted into `run_challenge_loop()` so tests can call it directly
without touching ConfigStore or performing network I/O.
"""

import json
import logging

import httpx
from InquirerPy import inquirer
from rich.console import Console
from rich.prompt import Confirm, Prompt

from skilark_cli.answer_checker import check_answer
from skilark_cli.client import SkilarkClient
from skilark_cli.config_store import ConfigStore
from skilark_cli.display import render_challenge, render_coding_problem, render_hint, render_result, render_test_results
from skilark_cli.editor import open_in_editor
from skilark_cli.executor import build_python_harness, run_code_local
from skilark_cli.onboarding import run_onboarding

console = Console()


def run() -> None:
    """Entry point called from main.py for `skilark today`."""
    config_store = ConfigStore()

    if config_store.is_first_run():
        run_onboarding(config_store, api_url="https://api.skilark.com")

    config = config_store.load()
    client = SkilarkClient(base_url=config["api_url"], user_id=config["user_id"])

    # Use total_completed to drive the displayed day counter.  If the stats
    # endpoint is unavailable (e.g. no network), fall back to day 1 silently
    # so the user can still practice.
    try:
        stats = client.get_stats()
        day = stats.get("total_completed", 0) + 1
    except httpx.HTTPError:
        # Expected when offline or the API is temporarily down — silently fall back.
        day = 1
    except Exception:
        # Unexpected error (e.g. parsing failure). Log it but keep the CLI usable.
        logging.getLogger(__name__).debug("Unexpected error fetching stats", exc_info=True)
        day = 1

    coding_language = config.get("coding_language")
    run_challenge_loop(client, topics=config["topics"], day=day, coding_language=coding_language)


def run_coding_flow(client: SkilarkClient, problem: dict, *, language: str, day: int) -> str:
    """Run the coding problem flow: display, edit, execute, submit.

    Presents the problem, then loops through action selections (solve, hint,
    skip, quit).  On a solve attempt, opens the editor, runs the code, renders
    results, and submits on success.  Failed attempts loop back to a retry menu.

    Args:
        client:   SkilarkClient used to submit the completed problem.
        problem:  Coding problem dict from get_next_coding_problem.
        language: Language slug the user will code in (e.g. "python").
        day:      Current challenge day number for the display header.

    Returns:
        "quit" if the user chose to quit, "done" otherwise.
    """
    render_coding_problem(console, problem, day=day)

    # Normalise languages field — may arrive as a JSON string or a dict.
    languages = problem.get("languages") or {}
    if isinstance(languages, str):
        try:
            languages = json.loads(languages)
        except (json.JSONDecodeError, TypeError):
            languages = {}

    # Normalise test_cases field — may arrive as a JSON string or a list.
    test_cases_raw = problem.get("test_cases") or []
    if isinstance(test_cases_raw, str):
        try:
            test_cases_raw = json.loads(test_cases_raw)
        except (json.JSONDecodeError, TypeError):
            test_cases_raw = []

    # Normalise hints field — may arrive as a JSON string or a list.
    hints_raw = problem.get("hints") or []
    if isinstance(hints_raw, str):
        try:
            hints_raw = json.loads(hints_raw)
        except (json.JSONDecodeError, TypeError):
            hints_raw = []

    # Extract language-specific starter code and function name.
    lang_config = languages.get(language, {})
    function_name: str = lang_config.get("function_name", "solution")
    starter_code: str = lang_config.get("starter_code", f"def {function_name}():\n    pass\n")

    # Reformat test cases for the harness: harness expects "expected_output" key.
    # Preserve the native type (list, int, bool, etc.) so the harness can do
    # direct Python equality comparison rather than comparing string repr.
    test_cases: list[dict] = []
    for tc in test_cases_raw:
        test_cases.append({
            "input": tc.get("input", {}),
            "expected_output": tc.get("expected"),
            "hidden": bool(tc.get("hidden", False)),
        })

    explanation: str = problem.get("explanation", "")
    problem_id: str = problem.get("id", "")

    attempt_count = 0
    last_passed_count = 0
    last_total_count = 0
    hint_idx = 0
    # user_code holds the last-edited code so retries pre-populate with their work.
    user_code = starter_code

    first_attempt = True
    while True:
        # Build action menu choices based on attempt history.
        if first_attempt:
            choices = [
                {"name": "Solve  (open editor)", "value": "solve"},
                {"name": "Hint", "value": "hint"},
                {"name": "Skip", "value": "skip"},
                {"name": "Quit", "value": "quit"},
            ]
        else:
            choices = [
                {"name": "Try again  (reopen editor)", "value": "retry"},
                {"name": "See explanation", "value": "explain"},
                {"name": "Skip", "value": "skip"},
                {"name": "Quit", "value": "quit"},
            ]

        action = inquirer.select(
            message="What would you like to do?",
            choices=choices,
        ).execute()

        if action == "quit":
            if attempt_count > 0:
                try:
                    client.submit_coding_problem(
                        problem_id,
                        language=language,
                        all_passed=False,
                        passed_count=last_passed_count,
                        total_count=last_total_count,
                    )
                except Exception:
                    pass
            return "quit"

        if action == "skip":
            if attempt_count > 0:
                try:
                    client.submit_coding_problem(
                        problem_id,
                        language=language,
                        all_passed=False,
                        passed_count=last_passed_count,
                        total_count=last_total_count,
                    )
                except Exception:
                    pass
            console.print("[dim]  Skipped.[/dim]\n")
            return "done"

        if action == "hint":
            if hint_idx < len(hints_raw):
                render_hint(console, hints_raw[hint_idx])
                hint_idx += 1
            else:
                console.print("[dim]  No more hints.[/dim]\n")
            # Loop back so the user can choose again after seeing the hint.
            continue

        if action == "explain":
            if explanation:
                console.print()
                console.print(f"[bold]Explanation:[/bold] {explanation}")
                console.print()
            else:
                console.print("[dim]  No explanation available.[/dim]\n")
            # Stay in loop — let them try again or skip after reading.
            continue

        # Gate languages we cannot execute locally before opening the editor.
        if language != "python":
            console.print(
                f"[yellow]  {language} execution is not yet supported. "
                "Use Python for now.[/yellow]"
            )
            return "done"

        # "solve" or "retry" — open editor and run code.
        user_code = open_in_editor(user_code, language)
        attempt_count += 1
        first_attempt = False

        harness = build_python_harness(user_code, function_name, test_cases)
        result = run_code_local(language, harness)

        render_test_results(console, result)

        all_passed: bool = result.get("all_passed", False)
        passed_count: int = result.get("passed_count", 0)
        total_count: int = result.get("total_count", 0)
        last_passed_count = passed_count
        last_total_count = total_count

        if all_passed:
            # Show explanation on success, then submit and return.
            if explanation:
                console.print()
                console.print(f"[bold]Explanation:[/bold] {explanation}")
                console.print()

            try:
                client.submit_coding_problem(
                    problem_id,
                    language=language,
                    all_passed=True,
                    passed_count=passed_count,
                    total_count=total_count,
                )
            except Exception:
                console.print("[dim]  (Could not record submission — try again later)[/dim]")
            return "done"

        # Partial pass — loop back with retry menu.
        continue


def run_challenge_loop(
    client: SkilarkClient,
    topics: list[str],
    day: int,
    coding_language: str | None = None,
) -> None:
    """Interactive challenge loop.

    Fetches one challenge at a time, handles the answer/hint/skip/quit
    interactions, records completions, and asks whether to continue.

    When *coding_language* is set and "dsa" is in *topics*, attempts to fetch
    and run a coding problem first.  Falls back to the predict-the-output flow
    if no coding problem is available.

    Separated from `run()` so tests can inject a mock client and drive the
    loop via patched Rich prompts without touching the filesystem or network.

    Args:
        client:          SkilarkClient instance (or mock in tests).
        topics:          Topic filter passed to get_next_challenge.
        day:             Starting day number displayed in the challenge header.
        coding_language: If set, enables coding problem dispatch for DSA topics.
    """
    while True:
        # Attempt coding problem dispatch when language preference is configured
        # and the user has DSA in their topics.
        if coding_language and "dsa" in topics:
            coding_problem = client.get_next_coding_problem(
                topics=topics, language=coding_language
            )
            if coding_problem is not None:
                outcome = run_coding_flow(
                    client, coding_problem, language=coding_language, day=day
                )
                if outcome == "quit":
                    return
                day += 1
                if not Confirm.ask("\n  Another?", default=True):
                    console.print("[dim]  See you tomorrow.[/dim]\n")
                    return
                continue

        # Fall through to predict-the-output challenge.
        challenge = client.get_next_challenge(topics=topics)
        if not challenge:
            console.print(
                "\n[dim]No more challenges available for your topics. "
                "Try adding more![/dim]\n"
            )
            return

        render_challenge(console, challenge, day=day)

        # Hints may arrive as a JSON string or as an already-parsed list.
        # Normalise to a list here so the rest of the loop is clean.
        hints = challenge.get("hints") or []
        if isinstance(hints, str):
            try:
                hints = json.loads(hints)
            except (json.JSONDecodeError, TypeError):
                hints = []

        hint_idx = 0

        while True:
            answer = Prompt.ask("  Your answer")

            if answer.lower() == "h":
                if hint_idx < len(hints):
                    render_hint(console, hints[hint_idx])
                    hint_idx += 1
                else:
                    console.print("[dim]  No more hints.[/dim]\n")
                continue

            if answer.lower() == "s":
                console.print("[dim]  Skipped.[/dim]\n")
                break

            if answer.lower() == "q":
                return

            correct = check_answer(answer, challenge["expected_answer"])
            self_corrected = False

            if not correct:
                render_result(
                    console,
                    correct=False,
                    expected_answer=challenge["expected_answer"],
                    explanation=challenge["explanation"],
                    deep_link=challenge["deep_link"],
                )
                self_corrected = Confirm.ask(
                    "  Were you actually right?", default=False
                )
                if self_corrected:
                    correct = True
            else:
                render_result(
                    console,
                    correct=True,
                    explanation=challenge["explanation"],
                    deep_link=challenge["deep_link"],
                )

            client.complete_challenge(
                challenge["id"],
                answer=answer,
                correct=correct,
                self_corrected=self_corrected,
            )
            break

        day += 1

        if not Confirm.ask("\n  Another?", default=True):
            console.print("[dim]  See you tomorrow.[/dim]\n")
            return
