import asyncio
import logging
import uuid
import socket
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

import yaml
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from api.database import get_session
from api.events import EventEnvelope, publish_event
from api.models.managed_repo import ManagedRepo
from api.models.workplan import Workplan
from api.schemas.workplan import (
    WorkplanBindingsSync,
    WorkplanCreate,
    WorkplanRead,
    WorkplanUpdate,
)
from api.services.lifecycle import transition_workplan_status
from api.services.legacy_meter import (
    LegacyUsageIdentity,
    identity_from_request,
    record_legacy_usage,
)
from api.workplan_status import (
    is_supported_workplan_status,
    normalize_workplan_status,
    ready_review_status,
)

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/workstreams", tags=["workstreams"])
workplan_router = APIRouter(prefix="/workplans", tags=["workplans"])

_INDEX_CACHE: dict[str, Any] | None = None
_INDEX_CACHE_AT: float = 0.0
_INDEX_TTL = 30.0
_INDEX_REFRESH_TASK: asyncio.Task | None = None
_INDEX_LAST_ERROR: str | None = None

_LEGACY_OWNER = "state-hub.api"
_COMPLETED_WORKSTREAM_EVENT = "org.statehub.workstream.completed"
_COMPLETED_WORKPLAN_EVENT = "org.statehub.workplan.completed"


def _repo_path(repo: ManagedRepo) -> Path | None:
    hostname = socket.gethostname()
    candidates = []
    host_paths = repo.host_paths or {}
    if host_paths.get(hostname):
        candidates.append(host_paths[hostname])
    if repo.local_path:
        candidates.append(repo.local_path)
    for raw in candidates:
        path = Path(raw).expanduser()
        if path.is_dir():
            return path
    return None


def _frontmatter(path: Path) -> dict[str, Any]:
    try:
        text = path.read_text(encoding="utf-8")
    except OSError:
        return {}
    if not text.startswith("---\n"):
        return {}
    end = text.find("\n---", 4)
    if end == -1:
        return {}

    try:
        return yaml.safe_load(text[4:end].strip()) or {}
    except yaml.YAMLError:
        return {}


def _legacy_key(method: str, route: str) -> str:
    return f"rest_api:{method} {route}"


def _mark_legacy_response(response: Response | None, replacement_ref: str) -> None:
    if response is None:
        return
    response.headers["Deprecation"] = "true"
    response.headers["X-StateHub-Replacement"] = replacement_ref
    response.headers.append("Link", f"<{replacement_ref}>; rel=\"successor-version\"")


async def _meter_legacy_route(
    *,
    session: AsyncSession,
    request: Request | None,
    response: Response | None,
    interface_key: str,
    replacement_ref: str,
) -> None:
    _mark_legacy_response(response, replacement_ref)
    try:
        await record_legacy_usage(
            session,
            interface_key=interface_key,
            interface_kind="rest_api",
            replacement_ref=replacement_ref,
            owner_component=_LEGACY_OWNER,
            replacement_verified=True,
            identity=identity_from_request(request),
        )
    except Exception:
        await session.rollback()
        logger.warning("legacy-meter failed to record %s", interface_key, exc_info=True)


async def _meter_legacy_event(
    *,
    session: AsyncSession,
    subject: str,
    replacement_ref: str,
) -> None:
    try:
        await record_legacy_usage(
            session,
            interface_key=f"event_subject:{subject}",
            interface_kind="event_subject",
            replacement_ref=replacement_ref,
            owner_component="state-hub.events",
            replacement_verified=True,
            identity=LegacyUsageIdentity(component_key="state-hub.events"),
        )
    except Exception:
        await session.rollback()
        logger.warning("legacy-meter failed to record event subject %s", subject, exc_info=True)


async def _list_workplans(
    *,
    topic_id: uuid.UUID | None,
    repo_id: uuid.UUID | None,
    repo_goal_id: uuid.UUID | None,
    status_filter: str | None,
    owner: str | None,
    slug: str | None,
    session: AsyncSession,
) -> list[Workplan]:
    q = select(Workplan)
    if topic_id:
        q = q.where(Workplan.topic_id == topic_id)
    if repo_id:
        q = q.where(Workplan.repo_id == repo_id)
    if repo_goal_id:
        q = q.where(Workplan.repo_goal_id == repo_goal_id)
    if status_filter:
        normalised_status = normalize_workplan_status(status_filter)
        if not is_supported_workplan_status(status_filter):
            raise HTTPException(status_code=422, detail=f"Unsupported workplan status '{status_filter}'")
        q = q.where(Workplan.status == normalised_status)
    if owner:
        q = q.where(Workplan.owner == owner)
    if slug:
        q = q.where(Workplan.slug == slug)
    q = q.order_by(
        Workplan.planning_priority.asc().nullslast(),
        Workplan.planning_order.asc().nullslast(),
        Workplan.updated_at.desc(),
    )
    result = await session.execute(q)
    return list(result.scalars().all())


async def _build_workplan_index(session: AsyncSession) -> dict[str, Any]:
    result = await session.execute(
        select(ManagedRepo).where(ManagedRepo.status == "active").order_by(ManagedRepo.slug)
    )
    index: dict[str, Any] = {}
    for repo in result.scalars().all():
        root = _repo_path(repo)
        if root is None:
            continue
        for directory, archived in (
            (root / "workplans", False),
            (root / "workplans" / "archived", True),
        ):
            if not directory.is_dir():
                continue
            for path in sorted(directory.glob("*.md")):
                data = _frontmatter(path)
                workplan_id = data.get("state_hub_workstream_id") or data.get("state_hub_workplan_id")
                if not workplan_id:
                    continue
                file_status = normalize_workplan_status(data.get("status", ""))
                review = (
                    ready_review_status(
                        root,
                        data.get("reviewed_against_commit"),
                        data.get("context_paths"),
                    )
                    if file_status == "ready"
                    else None
                )
                index[str(workplan_id)] = {
                    "filename": path.name,
                    "relative_path": str(path.relative_to(root)),
                    "repo_slug": repo.slug,
                    "archived": archived,
                    "status": file_status or None,
                    "needs_review": bool(review and review.needs_review),
                    "health_labels": ["needs_review"] if review and review.needs_review else [],
                }
    await _merge_db_backing_index(session, index)
    return {"workplans": index, "workstreams": index}


async def _merge_db_backing_index(session: AsyncSession, index: dict[str, Any]) -> None:
    """Fill index gaps from DB-backed file bindings synced by fix-consistency."""
    result = await session.execute(
        select(Workplan, ManagedRepo.slug)
        .join(ManagedRepo, Workplan.repo_id == ManagedRepo.id)
        .where(Workplan.backing_filename.isnot(None))
    )
    for wp, repo_slug in result.all():
        key = str(wp.id)
        if key in index:
            continue
        index[key] = {
            "filename": wp.backing_filename,
            "relative_path": wp.backing_relative_path,
            "repo_slug": repo_slug,
            "archived": bool(wp.backing_archived),
            "status": normalize_workplan_status(wp.status) if wp.status else None,
            "needs_review": False,
            "health_labels": [],
        }


def _invalidate_workplan_index_cache() -> None:
    global _INDEX_CACHE, _INDEX_CACHE_AT
    _INDEX_CACHE = None
    _INDEX_CACHE_AT = 0.0


def _index_with_meta(*, stale: bool, refresh_in_progress: bool) -> dict[str, Any]:
    age = time.monotonic() - _INDEX_CACHE_AT if _INDEX_CACHE_AT else None
    return {
        **(_INDEX_CACHE or {"workplans": {}, "workstreams": {}}),
        "_meta": {
            "generated_at": _INDEX_CACHE.get("_meta", {}).get("generated_at") if _INDEX_CACHE else None,
            "stale": stale,
            "cache_age_seconds": round(age, 3) if age is not None else None,
            "refresh_in_progress": refresh_in_progress,
            "last_error": _INDEX_LAST_ERROR,
        },
    }


async def _refresh_workplan_index_background() -> None:
    global _INDEX_CACHE, _INDEX_CACHE_AT, _INDEX_LAST_ERROR
    from api.database import async_session_factory

    try:
        async with async_session_factory() as session:
            index = await _build_workplan_index(session)
        index["_meta"] = {
            "generated_at": datetime.now(timezone.utc).isoformat(),
            "stale": False,
            "cache_age_seconds": 0.0,
            "refresh_in_progress": False,
            "last_error": None,
        }
        _INDEX_CACHE = index
        _INDEX_CACHE_AT = time.monotonic()
        _INDEX_LAST_ERROR = None
    except Exception as exc:
        _INDEX_LAST_ERROR = str(exc)


def _ensure_index_refresh_started() -> None:
    global _INDEX_REFRESH_TASK
    if _INDEX_REFRESH_TASK is not None and not _INDEX_REFRESH_TASK.done():
        return
    _INDEX_REFRESH_TASK = asyncio.create_task(_refresh_workplan_index_background())


async def _workplan_index(
    *,
    refresh: bool,
    session: AsyncSession,
) -> dict[str, Any]:
    """Map file-backed workplan ids to their local workplan filenames."""
    global _INDEX_CACHE, _INDEX_CACHE_AT, _INDEX_LAST_ERROR
    cache_age = time.monotonic() - _INDEX_CACHE_AT if _INDEX_CACHE_AT else None
    if not refresh and _INDEX_CACHE is not None and cache_age is not None and cache_age < _INDEX_TTL:
        refresh_running = _INDEX_REFRESH_TASK is not None and not _INDEX_REFRESH_TASK.done()
        return _index_with_meta(stale=False, refresh_in_progress=refresh_running)

    if not refresh and _INDEX_CACHE is not None:
        _ensure_index_refresh_started()
        return _index_with_meta(stale=True, refresh_in_progress=True)

    index = await _build_workplan_index(session)
    index["_meta"] = {
        "generated_at": datetime.now(timezone.utc).isoformat(),
        "stale": False,
        "cache_age_seconds": 0.0,
        "refresh_in_progress": False,
        "last_error": None,
    }
    _INDEX_CACHE = index
    _INDEX_CACHE_AT = time.monotonic()
    _INDEX_LAST_ERROR = None
    return _INDEX_CACHE


async def _create_workplan(
    *,
    body: WorkplanCreate,
    session: AsyncSession,
) -> Workplan:
    wp = Workplan(**body.model_dump())
    session.add(wp)
    await session.commit()
    await session.refresh(wp)
    return wp


async def _get_workplan(
    *,
    workplan_id: uuid.UUID,
    session: AsyncSession,
) -> Workplan:
    wp = await session.get(Workplan, workplan_id)
    if wp is None:
        raise HTTPException(status_code=404, detail="Workplan not found")
    return wp


async def _update_workplan(
    *,
    workplan_id: uuid.UUID,
    body: WorkplanUpdate,
    session: AsyncSession,
) -> Workplan:
    wp = await session.get(Workplan, workplan_id)
    if wp is None:
        raise HTTPException(status_code=404, detail="Workplan not found")
    update_data = body.model_dump(exclude_unset=True)
    status_update = update_data.pop("status", None)
    prev_status = wp.status
    for field, value in update_data.items():
        setattr(wp, field, value)
    if status_update is not None:
        transition_workplan_status(wp, status_update)
    await session.commit()
    await session.refresh(wp)

    if normalize_workplan_status(prev_status) != "finished" and wp.status == "finished":
        await _publish_completion_events(wp, session)

    return wp


async def _archive_workplan(
    *,
    workplan_id: uuid.UUID,
    session: AsyncSession,
) -> Workplan:
    wp = await session.get(Workplan, workplan_id)
    if wp is None:
        raise HTTPException(status_code=404, detail="Workplan not found")
    transition_workplan_status(wp, "archived")
    await session.commit()
    await session.refresh(wp)
    return wp


async def _publish_completion_events(wp: Workplan, session: AsyncSession) -> None:
    workplan_envelope = EventEnvelope.new(
        _COMPLETED_WORKPLAN_EVENT,
        attributes={
            "workplan_id": str(wp.id),
            "legacy_workstream_id": str(wp.id),
            "slug": wp.slug,
            "title": wp.title,
            "topic_id": str(wp.topic_id) if wp.topic_id else None,
            "repo_id": str(wp.repo_id) if wp.repo_id else None,
            "repo_goal_id": str(wp.repo_goal_id) if wp.repo_goal_id else None,
        },
    )
    asyncio.create_task(publish_event(_COMPLETED_WORKPLAN_EVENT, workplan_envelope))

    await _meter_legacy_event(
        session=session,
        subject=_COMPLETED_WORKSTREAM_EVENT,
        replacement_ref=_COMPLETED_WORKPLAN_EVENT,
    )
    legacy_envelope = EventEnvelope.new(
        _COMPLETED_WORKSTREAM_EVENT,
        attributes={
            "workstream_id": str(wp.id),
            "slug": wp.slug,
            "title": wp.title,
            "topic_id": str(wp.topic_id) if wp.topic_id else None,
            "repo_id": str(wp.repo_id) if wp.repo_id else None,
            "repo_goal_id": str(wp.repo_goal_id) if wp.repo_goal_id else None,
        },
    )
    asyncio.create_task(publish_event(_COMPLETED_WORKSTREAM_EVENT, legacy_envelope))


@router.get("/", response_model=list[WorkplanRead])
async def list_workstreams(
    request: Request,
    response: Response,
    topic_id: uuid.UUID | None = None,
    repo_id: uuid.UUID | None = None,
    repo_goal_id: uuid.UUID | None = None,
    status: str | None = None,
    owner: str | None = None,
    slug: str | None = None,
    session: AsyncSession = Depends(get_session),
) -> list[Workplan]:
    await _meter_legacy_route(
        session=session,
        request=request,
        response=response,
        interface_key=_legacy_key("GET", "/workstreams/"),
        replacement_ref="/workplans/",
    )
    return await _list_workplans(
        topic_id=topic_id,
        repo_id=repo_id,
        repo_goal_id=repo_goal_id,
        status_filter=status,
        owner=owner,
        slug=slug,
        session=session,
    )


@workplan_router.get("/", response_model=list[WorkplanRead])
async def list_workplans(
    topic_id: uuid.UUID | None = None,
    repo_id: uuid.UUID | None = None,
    repo_goal_id: uuid.UUID | None = None,
    status: str | None = None,
    owner: str | None = None,
    slug: str | None = None,
    session: AsyncSession = Depends(get_session),
) -> list[Workplan]:
    return await _list_workplans(
        topic_id=topic_id,
        repo_id=repo_id,
        repo_goal_id=repo_goal_id,
        status_filter=status,
        owner=owner,
        slug=slug,
        session=session,
    )


@router.get("/workplan-index")
async def workplan_index(
    request: Request,
    response: Response,
    refresh: bool = Query(False, description="Force cache invalidation"),
    session: AsyncSession = Depends(get_session),
) -> dict[str, Any]:
    await _meter_legacy_route(
        session=session,
        request=request,
        response=response,
        interface_key=_legacy_key("GET", "/workstreams/workplan-index"),
        replacement_ref="/workplans/index",
    )
    return await _workplan_index(refresh=refresh, session=session)


@workplan_router.get("/index")
async def workplan_index_preferred(
    refresh: bool = Query(False, description="Force cache invalidation"),
    session: AsyncSession = Depends(get_session),
) -> dict[str, Any]:
    return await _workplan_index(refresh=refresh, session=session)


@workplan_router.put("/index/bindings")
async def sync_workplan_bindings(
    body: WorkplanBindingsSync,
    session: AsyncSession = Depends(get_session),
) -> dict[str, int]:
    """Upsert workstation workplan file bindings for remote API index fallback."""
    synced_at = datetime.now(timezone.utc)
    updated = 0
    for entry in body.bindings:
        wp = await session.get(Workplan, entry.workplan_id)
        if wp is None:
            continue
        wp.backing_filename = entry.filename
        wp.backing_relative_path = entry.relative_path
        wp.backing_archived = entry.archived
        wp.backing_synced_at = synced_at
        updated += 1
    await session.commit()
    _invalidate_workplan_index_cache()
    return {"updated": updated, "received": len(body.bindings)}


@router.post("/", response_model=WorkplanRead, status_code=status.HTTP_201_CREATED)
async def create_workstream(
    request: Request,
    response: Response,
    body: WorkplanCreate,
    session: AsyncSession = Depends(get_session),
) -> Workplan:
    await _meter_legacy_route(
        session=session,
        request=request,
        response=response,
        interface_key=_legacy_key("POST", "/workstreams/"),
        replacement_ref="/workplans/",
    )
    return await _create_workplan(body=body, session=session)


@workplan_router.post("/", response_model=WorkplanRead, status_code=status.HTTP_201_CREATED)
async def create_workplan(
    body: WorkplanCreate,
    session: AsyncSession = Depends(get_session),
) -> Workplan:
    return await _create_workplan(body=body, session=session)


@router.get("/{workstream_id}", response_model=WorkplanRead)
async def get_workstream(
    request: Request,
    response: Response,
    workstream_id: uuid.UUID,
    session: AsyncSession = Depends(get_session),
) -> Workplan:
    await _meter_legacy_route(
        session=session,
        request=request,
        response=response,
        interface_key=_legacy_key("GET", "/workstreams/{workstream_id}"),
        replacement_ref="/workplans/{workplan_id}",
    )
    return await _get_workplan(workplan_id=workstream_id, session=session)


@workplan_router.get("/{workplan_id}", response_model=WorkplanRead)
async def get_workplan(
    workplan_id: uuid.UUID,
    session: AsyncSession = Depends(get_session),
) -> Workplan:
    return await _get_workplan(workplan_id=workplan_id, session=session)


@router.patch("/{workstream_id}", response_model=WorkplanRead)
async def update_workstream(
    request: Request,
    response: Response,
    workstream_id: uuid.UUID,
    body: WorkplanUpdate,
    session: AsyncSession = Depends(get_session),
) -> Workplan:
    await _meter_legacy_route(
        session=session,
        request=request,
        response=response,
        interface_key=_legacy_key("PATCH", "/workstreams/{workstream_id}"),
        replacement_ref="/workplans/{workplan_id}",
    )
    return await _update_workplan(workplan_id=workstream_id, body=body, session=session)


@workplan_router.patch("/{workplan_id}", response_model=WorkplanRead)
async def update_workplan(
    workplan_id: uuid.UUID,
    body: WorkplanUpdate,
    session: AsyncSession = Depends(get_session),
) -> Workplan:
    return await _update_workplan(workplan_id=workplan_id, body=body, session=session)


@router.delete("/{workstream_id}", response_model=WorkplanRead)
async def archive_workstream(
    request: Request,
    response: Response,
    workstream_id: uuid.UUID,
    session: AsyncSession = Depends(get_session),
) -> Workplan:
    await _meter_legacy_route(
        session=session,
        request=request,
        response=response,
        interface_key=_legacy_key("DELETE", "/workstreams/{workstream_id}"),
        replacement_ref="/workplans/{workplan_id}",
    )
    return await _archive_workplan(workplan_id=workstream_id, session=session)


@workplan_router.delete("/{workplan_id}", response_model=WorkplanRead)
async def archive_workplan(
    workplan_id: uuid.UUID,
    session: AsyncSession = Depends(get_session),
) -> Workplan:
    return await _archive_workplan(workplan_id=workplan_id, session=session)