from __future__ import annotations

import datetime
from collections import Counter
from typing import Annotated, Any

from fastapi import APIRouter, Header, HTTPException, Query
from pydantic import BaseModel

from matyan_backend.deps import FdbDb, KafkaProducerDep  # noqa: TC001
from matyan_backend.kafka.producer import emit_control_event
from matyan_backend.storage import entities
from matyan_backend.storage.runs import get_run_meta

from ._pydantic_models import (
    ExperimentActivityApiOut,
    ExperimentCreateRequest,
    ExperimentGetOut,
    ExperimentGetRunsResponse,
    ExperimentListOut,
    ExperimentUpdateOut,
    ExperimentUpdateRequest,
)

rest_router_experiments = APIRouter(prefix="/experiments")


class NoteIn(BaseModel):
    content: str


def _exp_to_out(exp: dict, run_count: int) -> dict:
    return {
        "id": exp["id"],
        "name": exp.get("name", ""),
        "description": exp.get("description", ""),
        "run_count": run_count,
        "archived": exp.get("is_archived", False),
        "creation_time": exp.get("created_at"),
    }


@rest_router_experiments.get("/", response_model=ExperimentListOut)
async def get_experiments_list_api(db: FdbDb) -> list[dict]:
    exps = entities.list_experiments(db)
    result: list[dict] = []
    for exp in exps:
        run_hashes = entities.get_runs_for_experiment(db, exp["id"])
        result.append(_exp_to_out(exp, run_count=len(run_hashes)))
    return result


@rest_router_experiments.get("/search/", response_model=ExperimentListOut)
async def search_experiments_by_name_api(
    db: FdbDb,
    q: Annotated[str | None, Query()] = None,
) -> list[dict]:
    exps = entities.list_experiments(db)
    search_term = q.strip().lower() if q else ""
    if search_term:
        exps = [e for e in exps if search_term in e.get("name", "").lower()]
    result: list[dict] = []
    for exp in exps:
        run_hashes = entities.get_runs_for_experiment(db, exp["id"])
        result.append(_exp_to_out(exp, run_count=len(run_hashes)))
    return result


@rest_router_experiments.post("/", response_model=ExperimentUpdateOut)
async def create_experiment_api(request: ExperimentCreateRequest, db: FdbDb) -> dict[str, Any]:
    try:
        exp = entities.create_experiment(db, name=request.name.strip())
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e)) from e
    return {"id": exp["id"], "status": "OK"}


@rest_router_experiments.get("/{uuid}/", response_model=ExperimentGetOut)
async def get_experiment_api(uuid: str, db: FdbDb) -> dict:
    exp = entities.get_experiment(db, uuid)
    if not exp:
        raise HTTPException(status_code=404)
    run_hashes = entities.get_runs_for_experiment(db, uuid)
    return _exp_to_out(exp, run_count=len(run_hashes))


@rest_router_experiments.delete("/{uuid}/")
async def delete_experiment_api(uuid: str, db: FdbDb, producer: KafkaProducerDep) -> dict[str, str]:
    exp = entities.get_experiment(db, uuid)
    if not exp:
        raise HTTPException(status_code=404)
    entities.delete_experiment(db, uuid)
    await emit_control_event(producer, "experiment_deleted", experiment_id=uuid)
    return {"status": "OK"}


@rest_router_experiments.put("/{uuid}/", response_model=ExperimentUpdateOut)
async def update_experiment_properties_api(
    uuid: str,
    exp_in: ExperimentUpdateRequest,
    db: FdbDb,
) -> dict[str, str]:
    exp = entities.get_experiment(db, uuid)
    if not exp:
        raise HTTPException(status_code=404)

    updates: dict = {}
    if exp_in.name:
        updates["name"] = exp_in.name.strip()
    if exp_in.description is not None:
        updates["description"] = exp_in.description
    if exp_in.archived is not None:
        updates["is_archived"] = exp_in.archived

    if updates:
        entities.update_experiment(db, uuid, **updates)

    return {"id": uuid, "status": "OK"}


@rest_router_experiments.get("/{uuid}/runs/", response_model=ExperimentGetRunsResponse)
async def get_experiment_runs_api(
    uuid: str,
    db: FdbDb,
    limit: Annotated[int | None, Query()] = None,
    offset: Annotated[str | None, Query()] = None,
) -> dict[str, Any]:
    exp = entities.get_experiment(db, uuid)
    if not exp:
        raise HTTPException(status_code=404)

    run_hashes = entities.get_runs_for_experiment(db, uuid)

    offset_idx = 0
    if offset and offset in run_hashes:
        offset_idx = run_hashes.index(offset) + 1
    run_hashes = run_hashes[offset_idx : offset_idx + limit] if limit else run_hashes[offset_idx:]

    runs = []
    for rh in run_hashes:
        meta = get_run_meta(db, rh)
        runs.append(
            {
                "run_id": rh,
                "name": meta.get("name", ""),
                "creation_time": meta.get("created_at", 0),
                "end_time": meta.get("finalized_at"),
                "archived": meta.get("is_archived", False),
            },
        )

    return {"id": uuid, "runs": runs}


# ---------------------------------------------------------------------------
# Notes
# ---------------------------------------------------------------------------


@rest_router_experiments.get("/{exp_id}/note/")
async def list_note_api(exp_id: str, db: FdbDb) -> list[dict]:
    exp = entities.get_experiment(db, exp_id)
    if not exp:
        raise HTTPException(status_code=404)
    return entities.list_notes_for_experiment(db, exp_id)


@rest_router_experiments.post("/{exp_id}/note/", status_code=201)
async def create_note_api(exp_id: str, note_in: NoteIn, db: FdbDb) -> dict[str, Any]:
    exp = entities.get_experiment(db, exp_id)
    if not exp:
        raise HTTPException(status_code=404)
    note = entities.create_note(db, note_in.content.strip(), experiment_id=exp_id)
    return {"id": note["id"], "created_at": note["created_at"]}


@rest_router_experiments.get("/{exp_id}/note/{_id}/")
async def get_note_api(exp_id: str, _id: str, db: FdbDb) -> dict[str, Any]:  # noqa: ARG001
    note = entities.get_note(db, _id)
    if not note:
        raise HTTPException(status_code=404)
    return {"id": note["id"], "content": note.get("content", ""), "updated_at": note.get("updated_at")}


@rest_router_experiments.put("/{exp_id}/note/{_id}/")
async def update_note_api(exp_id: str, _id: str, note_in: NoteIn, db: FdbDb) -> dict[str, Any]:  # noqa: ARG001
    note = entities.get_note(db, _id)
    if not note:
        raise HTTPException(status_code=404)
    entities.update_note(db, _id, content=note_in.content.strip())
    updated = entities.get_note(db, _id)
    if not updated:
        raise HTTPException(status_code=404)
    return {"id": _id, "content": updated.get("content", ""), "updated_at": updated.get("updated_at")}


@rest_router_experiments.delete("/{exp_id}/note/{_id}/")
async def delete_note_api(exp_id: str, _id: str, db: FdbDb) -> dict[str, str]:  # noqa: ARG001
    note = entities.get_note(db, _id)
    if not note:
        raise HTTPException(status_code=404)
    entities.delete_note(db, _id)
    return {"status": "OK"}


@rest_router_experiments.get("/{exp_id}/activity/", response_model=ExperimentActivityApiOut)
async def experiment_runs_activity_api(
    exp_id: str,
    db: FdbDb,
    x_timezone_offset: Annotated[int, Header()] = 0,
) -> dict[str, Any]:
    exp = entities.get_experiment(db, exp_id)
    if not exp:
        raise HTTPException(status_code=404)

    run_hashes = entities.get_runs_for_experiment(db, exp_id)

    num_runs = 0
    num_archived = 0
    num_active = 0
    activity_counter: Counter[str] = Counter()

    for rh in run_hashes:
        meta = get_run_meta(db, rh)
        num_runs += 1
        if meta.get("is_archived"):
            num_archived += 1
        if meta.get("active"):
            num_active += 1

        created = meta.get("created_at")
        if created:
            ts = created - x_timezone_offset * 60
            day = datetime.datetime.fromtimestamp(ts, tz=datetime.UTC).strftime("%Y-%m-%dT%H:00:00")
            activity_counter[day] += 1

    return {
        "num_runs": num_runs,
        "num_archived_runs": num_archived,
        "num_active_runs": num_active,
        "activity_map": dict(activity_counter),
    }
