"""A local fixture API. No live provider, arbitrary source fetch or student login."""

import logging
import secrets
from typing import Annotated
from uuid import uuid4

from fastapi import Depends, FastAPI, Request, Response
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from starlette.exceptions import HTTPException

from .core import build_evaluation, problem_for, transition_finding
from .errors import DomainError, Forbidden, Unauthorized
from .models import (
    Actor,
    Evaluation,
    EvaluationCreate,
    Finding,
    FindingCreate,
    Identifier,
    TransitionCreate,
)
from .transport import LocalTransport

# Public invented fixture credentials, NOT secrets/authenticated human identities.
FIXTURE_CREDENTIALS = {
    "fixture-author-a": Actor(id="author_A", role="author"),
    "fixture-author-b": Actor(id="author_B", role="author"),
    "fixture-reviewer-a": Actor(id="reviewer_A", role="reviewer"),
}


def create_app(
    repository,
    *,
    credentials,
    build=build_evaluation,
    problem=problem_for,
    transition=transition_finding,
):
    if type(credentials) is not dict or not 1 <= len(credentials) <= 10:
        raise ValueError("Explicit nonempty fixture credential map is required")
    configured = {}
    for token, actor in credentials.items():
        if (
            type(token) is not str
            or not 16 <= len(token) <= 64
            or not token.isascii()
            or not token.isprintable()
        ):
            raise ValueError("Fixture credential must be bounded printable ASCII")
        configured[token] = Actor.model_validate(actor)
    app = FastAPI(
        title="DVP local fixture evaluation service",
        description="Invented metadata/review teaching only. Do not expose publicly.",
        version="1.0.0",
    )
    app.state.repository = repository
    app.add_middleware(LocalTransport)

    bearer = HTTPBearer(
        auto_error=False,
        description="Public invented local-fixture credential, not production login.",
    )

    def actor_for(
        credential: Annotated[HTTPAuthorizationCredentials | None, Depends(bearer)],
    ) -> Actor:
        if credential is None or credential.scheme.lower() != "bearer":
            raise Unauthorized("Fixture credential is missing")
        supplied = credential.credentials
        if not supplied.isascii() or not 16 <= len(supplied) <= 64:
            raise Unauthorized("Invalid fixture credential")
        for token, actor in configured.items():
            if secrets.compare_digest(supplied, token):
                return actor
        raise Unauthorized("Unknown fixture credential")

    @app.exception_handler(DomainError)
    def domain_error(request: Request, error: DomainError):
        status, body = problem(error, request.state.request_id)
        return JSONResponse(
            body,
            status_code=status,
            headers={"WWW-Authenticate": "Bearer"} if status == 401 else {},
        )

    @app.exception_handler(RequestValidationError)
    def validation_error(request: Request, error: RequestValidationError):
        return JSONResponse(
            {
                "error": {
                    "code": "invalid_request",
                    "message": "Request does not match the documented schema.",
                    "request_id": request.state.request_id,
                }
            },
            status_code=422,
        )

    @app.exception_handler(HTTPException)
    def route_error(request: Request, error: HTTPException):
        code = "route_not_found" if error.status_code == 404 else "method_not_allowed"
        return JSONResponse(
            {
                "error": {
                    "code": code,
                    "message": "Use a documented route and method.",
                    "request_id": request.state.request_id,
                }
            },
            status_code=error.status_code,
            headers=error.headers,
        )

    @app.exception_handler(Exception)
    def internal_error(request: Request, error: Exception):
        # Never log body, Authorization, evidence or raw exception text here.
        logging.getLogger("dvp.evaluation").error(
            "internal_error request_id=%s", request.state.request_id
        )
        return JSONResponse(
            {
                "error": {
                    "code": "internal_error",
                    "message": "The service failed; retain the request ID and diagnose locally.",
                    "request_id": request.state.request_id,
                }
            },
            status_code=500,
            headers={
                "X-Request-ID": request.state.request_id,
                "Cache-Control": "no-store",
            },
        )

    @app.get("/health/live")
    def live():
        return {"status": "alive", "scope": "local_fixture_only"}

    @app.post("/evaluations", response_model=Evaluation, status_code=201)
    def create_evaluation(
        body: EvaluationCreate,
        actor: Annotated[Actor, Depends(actor_for)],
        response: Response,
    ):
        result = Evaluation.model_validate(build(body, actor, uuid4().hex))
        repository.put_evaluation(result)
        response.headers["Location"] = "/evaluations/" + result.id
        return result

    @app.get("/evaluations/{evaluation_id}", response_model=Evaluation)
    def get_evaluation(
        evaluation_id: Identifier, actor: Annotated[Actor, Depends(actor_for)]
    ):
        return repository.get_evaluation(evaluation_id)

    @app.post(
        "/evaluations/{evaluation_id}/findings", response_model=Finding, status_code=201
    )
    def add_finding(
        evaluation_id: Identifier,
        body: FindingCreate,
        actor: Annotated[Actor, Depends(actor_for)],
    ):
        if actor.role != "author":
            raise Forbidden("Only a configured author can draft a finding")
        result = Finding(
            **body.model_dump(),
            id=uuid4().hex,
            evaluation_id=evaluation_id,
            author_id=actor.id,
            state="draft",
            revision=1,
            reviewer_id=None,
        )
        repository.add_finding(result)
        return result

    @app.get(
        "/evaluations/{evaluation_id}/findings/{finding_id}", response_model=Finding
    )
    def get_finding(
        evaluation_id: Identifier,
        finding_id: Identifier,
        actor: Annotated[Actor, Depends(actor_for)],
    ):
        return repository.get_finding(evaluation_id, finding_id)

    @app.post(
        "/evaluations/{evaluation_id}/findings/{finding_id}/transitions",
        response_model=Finding,
    )
    def change_finding(
        evaluation_id: Identifier,
        finding_id: Identifier,
        body: TransitionCreate,
        actor: Annotated[Actor, Depends(actor_for)],
    ):
        return repository.apply_transition(
            evaluation_id,
            finding_id,
            body.action,
            actor,
            body.expected_revision,
            transition,
        )

    @app.get("/evaluations/{evaluation_id}/audit")
    def audit(evaluation_id: Identifier, actor: Annotated[Actor, Depends(actor_for)]):
        repository.get_evaluation(evaluation_id)
        return repository.audit(evaluation_id)

    return app
