from __future__ import annotations

from datetime import date
from types import SimpleNamespace
from typing import List

from fastapi import APIRouter, Depends, Form, HTTPException, Request, status
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates

from dependencies import (
    get_pricing_metadata,
    get_unit_of_work,
    require_any_role,
    require_roles,
    require_scenario_resource,
)
from models import ResourceType, Scenario, ScenarioStatus, User
from schemas.scenario import (
    ScenarioComparisonRequest,
    ScenarioComparisonResponse,
    ScenarioCreate,
    ScenarioRead,
    ScenarioUpdate,
)
from services.currency import CurrencyValidationError, normalise_currency
from services.exceptions import (
    EntityConflictError,
    EntityNotFoundError,
    ScenarioValidationError,
)
from services.pricing import PricingMetadata
from services.unit_of_work import UnitOfWork

router = APIRouter(tags=["Scenarios"])
templates = Jinja2Templates(directory="templates")

READ_ROLES = ("viewer", "analyst", "project_manager", "admin")
MANAGE_ROLES = ("project_manager", "admin")


def _to_read_model(scenario: Scenario) -> ScenarioRead:
    return ScenarioRead.model_validate(scenario)


def _resource_type_choices() -> list[tuple[str, str]]:
    return [
        (resource.value, resource.value.replace("_", " ").title())
        for resource in ResourceType
    ]


def _scenario_status_choices() -> list[tuple[str, str]]:
    return [
        (status.value, status.value.title()) for status in ScenarioStatus
    ]


def _require_project_repo(uow: UnitOfWork):
    if not uow.projects:
        raise RuntimeError("Project repository not initialised")
    return uow.projects


def _require_scenario_repo(uow: UnitOfWork):
    if not uow.scenarios:
        raise RuntimeError("Scenario repository not initialised")
    return uow.scenarios


@router.get(
    "/projects/{project_id}/scenarios",
    response_model=List[ScenarioRead],
)
def list_scenarios_for_project(
    project_id: int,
    _: User = Depends(require_any_role(*READ_ROLES)),
    uow: UnitOfWork = Depends(get_unit_of_work),
) -> List[ScenarioRead]:
    project_repo = _require_project_repo(uow)
    scenario_repo = _require_scenario_repo(uow)
    try:
        project_repo.get(project_id)
    except EntityNotFoundError as exc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc

    scenarios = scenario_repo.list_for_project(project_id)
    return [_to_read_model(scenario) for scenario in scenarios]


@router.post(
    "/projects/{project_id}/scenarios/compare",
    response_model=ScenarioComparisonResponse,
    status_code=status.HTTP_200_OK,
)
def compare_scenarios(
    project_id: int,
    payload: ScenarioComparisonRequest,
    _: User = Depends(require_any_role(*READ_ROLES)),
    uow: UnitOfWork = Depends(get_unit_of_work),
) -> ScenarioComparisonResponse:
    try:
        _require_project_repo(uow).get(project_id)
    except EntityNotFoundError as exc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
        ) from exc

    try:
        scenarios = uow.validate_scenarios_for_comparison(payload.scenario_ids)
        if any(scenario.project_id != project_id for scenario in scenarios):
            raise ScenarioValidationError(
                code="SCENARIO_PROJECT_MISMATCH",
                message="Selected scenarios do not belong to the same project.",
                scenario_ids=[
                    scenario.id for scenario in scenarios if scenario.id is not None
                ],
            )
    except EntityNotFoundError as exc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
        ) from exc
    except ScenarioValidationError as exc:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
            detail={
                "code": exc.code,
                "message": exc.message,
                "scenario_ids": list(exc.scenario_ids or []),
            },
        ) from exc

    return ScenarioComparisonResponse(
        project_id=project_id,
        scenarios=[_to_read_model(scenario) for scenario in scenarios],
    )


@router.post(
    "/projects/{project_id}/scenarios",
    response_model=ScenarioRead,
    status_code=status.HTTP_201_CREATED,
)
def create_scenario_for_project(
    project_id: int,
    payload: ScenarioCreate,
    _: User = Depends(require_roles(*MANAGE_ROLES)),
    uow: UnitOfWork = Depends(get_unit_of_work),
    metadata: PricingMetadata = Depends(get_pricing_metadata),
) -> ScenarioRead:
    project_repo = _require_project_repo(uow)
    scenario_repo = _require_scenario_repo(uow)
    try:
        project_repo.get(project_id)
    except EntityNotFoundError as exc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc

    scenario_data = payload.model_dump()
    if not scenario_data.get("currency") and metadata.default_currency:
        scenario_data["currency"] = metadata.default_currency
    scenario = Scenario(project_id=project_id, **scenario_data)

    try:
        created = scenario_repo.create(scenario)
    except EntityConflictError as exc:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
    return _to_read_model(created)


@router.get("/scenarios/{scenario_id}", response_model=ScenarioRead)
def get_scenario(
    scenario: Scenario = Depends(require_scenario_resource()),
) -> ScenarioRead:
    return _to_read_model(scenario)


@router.put("/scenarios/{scenario_id}", response_model=ScenarioRead)
def update_scenario(
    payload: ScenarioUpdate,
    scenario: Scenario = Depends(
        require_scenario_resource(require_manage=True)
    ),
    uow: UnitOfWork = Depends(get_unit_of_work),
) -> ScenarioRead:
    update_data = payload.model_dump(exclude_unset=True)
    for field, value in update_data.items():
        setattr(scenario, field, value)

    uow.flush()
    return _to_read_model(scenario)


@router.delete("/scenarios/{scenario_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_scenario(
    scenario: Scenario = Depends(
        require_scenario_resource(require_manage=True)
    ),
    uow: UnitOfWork = Depends(get_unit_of_work),
) -> None:
    _require_scenario_repo(uow).delete(scenario.id)


def _normalise(value: str | None) -> str | None:
    if value is None:
        return None
    value = value.strip()
    return value or None


def _parse_date(value: str | None) -> date | None:
    value = _normalise(value)
    if not value:
        return None
    return date.fromisoformat(value)


def _parse_discount_rate(value: str | None) -> float | None:
    value = _normalise(value)
    if not value:
        return None
    try:
        return float(value)
    except ValueError:
        return None


def _scenario_form_state(
    *,
    project_id: int,
    name: str,
    description: str | None,
    status: ScenarioStatus,
    start_date: date | None,
    end_date: date | None,
    discount_rate: float | None,
    currency: str | None,
    primary_resource: ResourceType | None,
    scenario_id: int | None = None,
) -> SimpleNamespace:
    return SimpleNamespace(
        id=scenario_id,
        project_id=project_id,
        name=name,
        description=description,
        status=status,
        start_date=start_date,
        end_date=end_date,
        discount_rate=discount_rate,
        currency=currency,
        primary_resource=primary_resource,
    )


@router.get(
    "/projects/{project_id}/scenarios/new",
    response_class=HTMLResponse,
    include_in_schema=False,
    name="scenarios.create_scenario_form",
)
def create_scenario_form(
    project_id: int,
    request: Request,
    _: User = Depends(require_roles(*MANAGE_ROLES)),
    uow: UnitOfWork = Depends(get_unit_of_work),
    metadata: PricingMetadata = Depends(get_pricing_metadata),
) -> HTMLResponse:
    try:
        project = _require_project_repo(uow).get(project_id)
    except EntityNotFoundError as exc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
        ) from exc

    return templates.TemplateResponse(
        request,
        "scenarios/form.html",
        {
            "project": project,
            "scenario": None,
            "scenario_statuses": _scenario_status_choices(),
            "resource_types": _resource_type_choices(),
            "form_action": request.url_for(
                "scenarios.create_scenario_submit", project_id=project_id
            ),
            "cancel_url": request.url_for(
                "projects.view_project", project_id=project_id
            ),
            "default_currency": metadata.default_currency,
        },
    )


@router.post(
    "/projects/{project_id}/scenarios/new",
    include_in_schema=False,
    name="scenarios.create_scenario_submit",
)
def create_scenario_submit(
    project_id: int,
    request: Request,
    _: User = Depends(require_roles(*MANAGE_ROLES)),
    name: str = Form(...),
    description: str | None = Form(None),
    status_value: str = Form(ScenarioStatus.DRAFT.value),
    start_date: str | None = Form(None),
    end_date: str | None = Form(None),
    discount_rate: str | None = Form(None),
    currency: str | None = Form(None),
    primary_resource: str | None = Form(None),
    uow: UnitOfWork = Depends(get_unit_of_work),
    metadata: PricingMetadata = Depends(get_pricing_metadata),
):
    project_repo = _require_project_repo(uow)
    scenario_repo = _require_scenario_repo(uow)
    try:
        project = project_repo.get(project_id)
    except EntityNotFoundError as exc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
        ) from exc

    try:
        status_enum = ScenarioStatus(status_value)
    except ValueError:
        status_enum = ScenarioStatus.DRAFT

    resource_enum = None
    if primary_resource:
        try:
            resource_enum = ResourceType(primary_resource)
        except ValueError:
            resource_enum = None

    name_value = name.strip()
    description_value = _normalise(description)
    start_date_value = _parse_date(start_date)
    end_date_value = _parse_date(end_date)
    discount_rate_value = _parse_discount_rate(discount_rate)
    currency_input = _normalise(currency)
    effective_currency = currency_input or metadata.default_currency

    try:
        currency_value = (
            normalise_currency(effective_currency)
            if effective_currency else None
        )
    except CurrencyValidationError as exc:
        form_state = _scenario_form_state(
            project_id=project_id,
            name=name_value,
            description=description_value,
            status=status_enum,
            start_date=start_date_value,
            end_date=end_date_value,
            discount_rate=discount_rate_value,
            currency=currency_input or metadata.default_currency,
            primary_resource=resource_enum,
        )
        return templates.TemplateResponse(
            request,
            "scenarios/form.html",
            {
                "project": project,
                "scenario": form_state,
                "scenario_statuses": _scenario_status_choices(),
                "resource_types": _resource_type_choices(),
                "form_action": request.url_for(
                    "scenarios.create_scenario_submit", project_id=project_id
                ),
                "cancel_url": request.url_for(
                    "projects.view_project", project_id=project_id
                ),
                "error": str(exc),
                "default_currency": metadata.default_currency,
            },
            status_code=status.HTTP_400_BAD_REQUEST,
        )

    scenario = Scenario(
        project_id=project_id,
        name=name_value,
        description=description_value,
        status=status_enum,
        start_date=start_date_value,
        end_date=end_date_value,
        discount_rate=discount_rate_value,
        currency=currency_value,
        primary_resource=resource_enum,
    )

    try:
        scenario_repo.create(scenario)
    except EntityConflictError:
        return templates.TemplateResponse(
            request,
            "scenarios/form.html",
            {
                "project": project,
                "scenario": scenario,
                "scenario_statuses": _scenario_status_choices(),
                "resource_types": _resource_type_choices(),
                "form_action": request.url_for(
                    "scenarios.create_scenario_submit", project_id=project_id
                ),
                "cancel_url": request.url_for(
                    "projects.view_project", project_id=project_id
                ),
                "error": "Scenario could not be created.",
                "default_currency": metadata.default_currency,
            },
            status_code=status.HTTP_409_CONFLICT,
        )

    return RedirectResponse(
        request.url_for("projects.view_project", project_id=project_id),
        status_code=status.HTTP_303_SEE_OTHER,
    )


@router.get(
    "/scenarios/{scenario_id}/view",
    response_class=HTMLResponse,
    include_in_schema=False,
    name="scenarios.view_scenario",
)
def view_scenario(
    request: Request,
    scenario: Scenario = Depends(
        require_scenario_resource(with_children=True)
    ),
    uow: UnitOfWork = Depends(get_unit_of_work),
) -> HTMLResponse:
    project = _require_project_repo(uow).get(scenario.project_id)
    financial_inputs = sorted(
        scenario.financial_inputs, key=lambda item: item.created_at
    )
    simulation_parameters = sorted(
        scenario.simulation_parameters, key=lambda item: item.created_at
    )

    scenario_metrics = {
        "financial_count": len(financial_inputs),
        "parameter_count": len(simulation_parameters),
        "currency": scenario.currency,
        "primary_resource": scenario.primary_resource.value.replace('_', ' ').title() if scenario.primary_resource else None,
    }

    return templates.TemplateResponse(
        request,
        "scenarios/detail.html",
        {
            "project": project,
            "scenario": scenario,
            "scenario_metrics": scenario_metrics,
            "financial_inputs": financial_inputs,
            "simulation_parameters": simulation_parameters,
        },
    )


@router.get(
    "/scenarios/{scenario_id}/edit",
    response_class=HTMLResponse,
    include_in_schema=False,
    name="scenarios.edit_scenario_form",
)
def edit_scenario_form(
    request: Request,
    scenario: Scenario = Depends(
        require_scenario_resource(require_manage=True)
    ),
    uow: UnitOfWork = Depends(get_unit_of_work),
    metadata: PricingMetadata = Depends(get_pricing_metadata),
) -> HTMLResponse:
    project = _require_project_repo(uow).get(scenario.project_id)

    return templates.TemplateResponse(
        request,
        "scenarios/form.html",
        {
            "project": project,
            "scenario": scenario,
            "scenario_statuses": _scenario_status_choices(),
            "resource_types": _resource_type_choices(),
            "form_action": request.url_for(
                "scenarios.edit_scenario_submit", scenario_id=scenario.id
            ),
            "cancel_url": request.url_for(
                "scenarios.view_scenario", scenario_id=scenario.id
            ),
            "default_currency": metadata.default_currency,
        },
    )


@router.post(
    "/scenarios/{scenario_id}/edit",
    include_in_schema=False,
    name="scenarios.edit_scenario_submit",
)
def edit_scenario_submit(
    request: Request,
    scenario: Scenario = Depends(
        require_scenario_resource(require_manage=True)
    ),
    name: str = Form(...),
    description: str | None = Form(None),
    status_value: str = Form(ScenarioStatus.DRAFT.value),
    start_date: str | None = Form(None),
    end_date: str | None = Form(None),
    discount_rate: str | None = Form(None),
    currency: str | None = Form(None),
    primary_resource: str | None = Form(None),
    uow: UnitOfWork = Depends(get_unit_of_work),
    metadata: PricingMetadata = Depends(get_pricing_metadata),
):
    project = _require_project_repo(uow).get(scenario.project_id)

    name_value = name.strip()
    description_value = _normalise(description)
    try:
        scenario.status = ScenarioStatus(status_value)
    except ValueError:
        scenario.status = ScenarioStatus.DRAFT
    status_enum = scenario.status

    resource_enum = None
    if primary_resource:
        try:
            resource_enum = ResourceType(primary_resource)
        except ValueError:
            resource_enum = None

    start_date_value = _parse_date(start_date)
    end_date_value = _parse_date(end_date)
    discount_rate_value = _parse_discount_rate(discount_rate)
    currency_input = _normalise(currency)

    try:
        currency_value = normalise_currency(currency_input)
    except CurrencyValidationError as exc:
        form_state = _scenario_form_state(
            scenario_id=scenario.id,
            project_id=scenario.project_id,
            name=name_value,
            description=description_value,
            status=status_enum,
            start_date=start_date_value,
            end_date=end_date_value,
            discount_rate=discount_rate_value,
            currency=currency_input,
            primary_resource=resource_enum,
        )
        return templates.TemplateResponse(
            request,
            "scenarios/form.html",
            {
                "project": project,
                "scenario": form_state,
                "scenario_statuses": _scenario_status_choices(),
                "resource_types": _resource_type_choices(),
                "form_action": request.url_for(
                    "scenarios.edit_scenario_submit", scenario_id=scenario.id
                ),
                "cancel_url": request.url_for(
                    "scenarios.view_scenario", scenario_id=scenario.id
                ),
                "error": str(exc),
                "default_currency": metadata.default_currency,
            },
            status_code=status.HTTP_400_BAD_REQUEST,
        )

    scenario.name = name_value
    scenario.description = description_value
    scenario.start_date = start_date_value
    scenario.end_date = end_date_value
    scenario.discount_rate = discount_rate_value
    scenario.currency = currency_value
    scenario.primary_resource = resource_enum

    uow.flush()

    return RedirectResponse(
        request.url_for("scenarios.view_scenario", scenario_id=scenario.id),
        status_code=status.HTTP_303_SEE_OTHER,
    )
