from __future__ import annotations

import configparser
import json
import time
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any

from .content import CarInfo, ContentLibrary, TrackLayoutInfo, build_content_library, load_setup_file
from .paths import EnvironmentPaths


@dataclass(slots=True)
class SetupProfile:
    profile_id: str
    name: str
    description: str
    target_fuel_l: int
    abs_target: int | None
    tc_target: int | None
    notes: list[str]


SETUP_PROFILES: dict[str, SetupProfile] = {
    "discipline_baseline": SetupProfile(
        profile_id="discipline_baseline",
        name="Discipline Baseline",
        description="Low-fuel baseline with conservative assists so you still have to drive the car.",
        target_fuel_l=12,
        abs_target=1,
        tc_target=1,
        notes=[
            "Low fuel makes mistakes obvious without burying you under tyre/fuel management noise.",
            "ABS/TC are reduced, not deleted, unless the car is already running without them.",
        ],
    ),
    "assist_fade": SetupProfile(
        profile_id="assist_fade",
        name="Assist Fade",
        description="One step closer to raw driving: enough support to keep reps clean, not enough to hide bad habits.",
        target_fuel_l=10,
        abs_target=0,
        tc_target=0,
        notes=[
            "Use this after you can repeat the baseline cleanly.",
            "If the car becomes survival-mode, go back one stage instead of brute-forcing it.",
        ],
    ),
    "race_discipline": SetupProfile(
        profile_id="race_discipline",
        name="Race Discipline",
        description="Short-fuel race-car profile that keeps you honest on brake release and exits.",
        target_fuel_l=18,
        abs_target=2,
        tc_target=1,
        notes=[
            "Enough fuel for a focused stint, not a wander.",
            "Assists stay low so the car still teaches you something.",
        ],
    ),
}

PREFERRED_TRACKS: dict[str, dict[str, float]] = {
    "smooth_inputs": {
        "ks_vallelunga/club_circuit": 2.5,
        "magione": 2.5,
        "ks_brands_hatch/indy": 2.0,
        "ks_red_bull_ring/layout_national": 1.5,
        "ks_silverstone/national": 1.0,
    },
    "rwd_momentum": {
        "ks_brands_hatch/indy": 2.5,
        "ks_vallelunga/classic_circuit": 2.0,
        "ks_nurburgring/layout_sprint_a": 1.5,
        "ks_red_bull_ring/layout_national": 1.0,
    },
    "race_braking": {
        "ks_nurburgring/layout_sprint_a": 2.5,
        "ks_red_bull_ring/layout_national": 2.0,
        "ks_silverstone/international": 1.5,
        "imola": 1.0,
    },
}

PREFERRED_CARS: dict[str, dict[str, float]] = {
    "smooth_inputs": {
        "abarth500": 3.0,
        "ks_abarth_595ss": 2.0,
    },
    "rwd_momentum": {
        "ks_mazda_rx7_spirit_r": 3.0,
        "j8_eunos_roadster_tuned": 2.0,
    },
    "race_braking": {
        "lotus_evora_gtc": 3.0,
        "bmw_z4_gt3": 2.0,
    },
}


@dataclass(slots=True)
class TrainingRecommendation:
    plan_id: str
    stage: str
    priority: int
    title: str
    car_model: str
    track: str
    track_layout: str | None
    setup_profile: str
    live_mode: str
    why: list[str]
    score: float

    def to_dict(self) -> dict[str, Any]:
        return asdict(self)


def _track_score(track: TrackLayoutInfo, *, preferred_length: tuple[float, float]) -> float:
    if not track.training_suitable:
        return -100.0
    score = 0.0
    length = track.length_m
    if length is None:
        return score
    lo, hi = preferred_length
    if lo <= length <= hi:
        score += 4.0
    elif lo * 0.7 <= length <= hi * 1.35:
        score += 2.0
    elif length > hi * 2.0:
        score -= 4.0
    normalized = {tag.lower() for tag in track.tags}
    if "drift" in normalized and "circuit" not in normalized:
        score -= 2.0
    if track.layout and any(token in track.layout.lower() for token in ("club", "indy", "national", "short", "sprint")):
        score += 1.5
    return score


def _car_score(
    car: CarInfo,
    *,
    power_range: tuple[float, float],
    preferred_drive: set[str],
    prefer_race: bool,
    require_setup: bool = True,
) -> float:
    score = 0.0
    if require_setup and not car.has_setup_seed:
        score -= 3.0
    if car.drive_layout in preferred_drive:
        score += 3.0
    elif car.drive_layout is not None:
        score -= 1.0
    if car.transmission == "manual":
        score += 1.5
    elif car.transmission == "semiautomatic":
        score += 0.5
    if car.bhp is not None:
        if power_range[0] <= car.bhp <= power_range[1]:
            score += 4.0
        elif power_range[0] * 0.7 <= car.bhp <= power_range[1] * 1.25:
            score += 2.0
        else:
            score -= 2.0
    if car.weight_kg is not None and 900 <= car.weight_kg <= 1350:
        score += 1.0
    normalized_tags = {tag.lower().lstrip("#") for tag in car.tags}
    if "drift" in normalized_tags:
        score -= 4.0
    if prefer_race:
        if "race" in normalized_tags or (car.car_class or "").lower() == "race":
            score += 2.5
    else:
        if "street" in normalized_tags or (car.car_class or "").lower() == "street":
            score += 1.5
    if car.official:
        score += 1.5
    return score


def recommend_training_curriculum(
    library: ContentLibrary,
    *,
    current_car_model: str | None = None,
    current_track: str | None = None,
    current_track_config: str | None = None,
) -> dict[str, Any]:
    stages = [
        {
            "stage": "smooth_inputs",
            "priority": 1,
            "title": "Smooth Inputs",
            "power_range": (120.0, 220.0),
            "preferred_drive": {"fwd"},
            "prefer_race": False,
            "preferred_length": (1500.0, 3200.0),
            "setup_profile": "discipline_baseline",
            "live_mode": "smoothness",
            "why_template": [
                "Short lap length forces repetition instead of sightseeing.",
                "Low-power front-drive cars punish impatience without being chaotic.",
            ],
        },
        {
            "stage": "rwd_momentum",
            "priority": 2,
            "title": "RWD Momentum",
            "power_range": (180.0, 320.0),
            "preferred_drive": {"rwd"},
            "prefer_race": False,
            "preferred_length": (1700.0, 4200.0),
            "setup_profile": "assist_fade",
            "live_mode": "exit",
            "why_template": [
                "This stage teaches throttle discipline and apex commitment.",
                "You have enough power to pay for exit mistakes, but not enough to hide them on the straights.",
            ],
        },
        {
            "stage": "race_braking",
            "priority": 3,
            "title": "Race Braking",
            "power_range": (350.0, 500.0),
            "preferred_drive": {"rwd"},
            "prefer_race": True,
            "preferred_length": (2200.0, 5000.0),
            "setup_profile": "race_discipline",
            "live_mode": "braking",
            "why_template": [
                "Race-car grip shifts the challenge from survival to brake release and commitment.",
                "Shorter race circuits make the same braking errors repeat often enough to fix them.",
            ],
        },
    ]

    recommendations: list[TrainingRecommendation] = []
    for stage in stages:
        ranked: list[tuple[float, CarInfo, TrackLayoutInfo]] = []
        for car in library.cars.values():
            car_score = _car_score(
                car,
                power_range=stage["power_range"],
                preferred_drive=stage["preferred_drive"],
                prefer_race=stage["prefer_race"],
            )
            if car_score < 1.0:
                continue
            for track in library.tracks.values():
                track_score = _track_score(track, preferred_length=stage["preferred_length"])
                if track_score < 0.0:
                    continue
                bonus = 0.0
                bonus += PREFERRED_CARS.get(stage["stage"], {}).get(car.folder_name, 0.0)
                bonus += PREFERRED_TRACKS.get(stage["stage"], {}).get(track.layout_id, 0.0)
                if current_car_model == car.folder_name:
                    bonus += 0.75
                if current_track == track.track and current_track_config == track.layout:
                    bonus += 0.5
                ranked.append((car_score + track_score + bonus, car, track))
        ranked.sort(key=lambda item: item[0], reverse=True)
        if not ranked:
            continue
        score, car, track = ranked[0]
        why = list(stage["why_template"])
        why.append(f"Selected combo: {car.name} on {track.name}.")
        if car.has_setup_seed:
            why.append("A saved setup seed exists locally, so the tool can materialize a training setup instead of just recommending one.")
        recommendations.append(
            TrainingRecommendation(
                plan_id=f"{stage['stage']}::{car.folder_name}::{track.layout_id}",
                stage=stage["stage"],
                priority=stage["priority"],
                title=stage["title"],
                car_model=car.folder_name,
                track=track.track,
                track_layout=track.layout,
                setup_profile=stage["setup_profile"],
                live_mode=stage["live_mode"],
                why=why,
                score=round(score, 2),
            )
        )

    current_combo = None
    if current_car_model and current_track:
        current_combo = {
            "car_model": current_car_model,
            "track": current_track,
            "track_layout": current_track_config,
            "available_setup_seed": bool(library.cars.get(current_car_model) and library.cars[current_car_model].has_setup_seed),
        }

    return {
        "summary": library.summary(),
        "current_combo": current_combo,
        "setup_profiles": {key: asdict(value) for key, value in SETUP_PROFILES.items()},
        "curriculum": [recommendation.to_dict() for recommendation in recommendations],
    }


def _pick_setup_seed(library: ContentLibrary, car_model: str, track: str | None, track_layout: str | None) -> Path:
    seeds = library.setup_seeds.get(car_model, [])
    if not seeds:
        raise RuntimeError(f"No saved setup seed exists for {car_model}")
    if track:
        track_tokens = {track.lower()}
        if track_layout:
            track_tokens.add(track_layout.lower())
        for seed in seeds:
            lowered = str(seed.path).lower()
            if all(token in lowered for token in track_tokens):
                return seed.path
    return seeds[0].path


def _update_numeric_value(parser: configparser.ConfigParser, section: str, value: int) -> bool:
    if not parser.has_section(section):
        return False
    parser.set(section, "VALUE", str(value))
    return True


def materialize_training_setup(
    paths: EnvironmentPaths,
    *,
    car_model: str,
    track: str | None,
    track_layout: str | None,
    profile_id: str,
    setup_name: str | None = None,
) -> dict[str, Any]:
    library = build_content_library(paths)
    if profile_id not in SETUP_PROFILES:
        raise RuntimeError(f"Unknown setup profile '{profile_id}'")
    profile = SETUP_PROFILES[profile_id]
    seed_path = _pick_setup_seed(library, car_model, track, track_layout)
    parser = load_setup_file(seed_path)
    changes: dict[str, Any] = {}

    if _update_numeric_value(parser, "FUEL", profile.target_fuel_l):
        changes["FUEL"] = profile.target_fuel_l
    if profile.abs_target is not None and parser.has_section("ABS"):
        current = parser.getint("ABS", "VALUE", fallback=profile.abs_target)
        target = min(current, profile.abs_target)
        parser.set("ABS", "VALUE", str(target))
        changes["ABS"] = target
    if profile.tc_target is not None and parser.has_section("TRACTION_CONTROL"):
        current = parser.getint("TRACTION_CONTROL", "VALUE", fallback=profile.tc_target)
        target = min(current, profile.tc_target)
        parser.set("TRACTION_CONTROL", "VALUE", str(target))
        changes["TRACTION_CONTROL"] = target

    output_dir = paths.documents_root / "setups" / car_model / "generic"
    if track:
        track_suffix = track if track_layout is None else f"{track}_{track_layout}"
        output_dir = paths.documents_root / "setups" / car_model / track_suffix
    output_dir.mkdir(parents=True, exist_ok=True)
    timestamp = time.strftime("%Y%m%d_%H%M%S")
    file_name = setup_name or f"codex_{profile_id}_{timestamp}.ini"
    if not file_name.lower().endswith(".ini"):
        file_name += ".ini"
    output_path = output_dir / file_name
    with output_path.open("w", encoding="utf-8") as handle:
        parser.write(handle, space_around_delimiters=False)

    return {
        "profile": asdict(profile),
        "seed_path": str(seed_path),
        "output_path": str(output_path),
        "changes": changes,
    }


def save_training_plan(paths: EnvironmentPaths, plan: dict[str, Any]) -> Path:
    paths.plans_root.mkdir(parents=True, exist_ok=True)
    timestamp = time.strftime("%Y%m%d-%H%M%S")
    plan_path = paths.plans_root / f"training-plan-{timestamp}.json"
    plan_path.write_text(json.dumps(plan, indent=2), encoding="utf-8")
    return plan_path
