"""FastAPI application module."""

from __future__ import annotations

import hashlib
import json
import uuid
from typing import Any, Dict, Optional

import structlog
from fastapi import Depends, FastAPI, Header, HTTPException, Request
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from packages.config import Settings, get_settings
from packages.db.models import AuditEvent, ProcessedWebhook, Run, WorkflowState
from packages.db.session import get_db_session, init_db
from packages.integrations.jira import get_jira_client
from packages.integrations.jira_webhook import (
    design_review_decision,
    doc_review_decision,
    duplicate_review_decision,
    normalize_jira_webhook,
    should_resume_clarification,
    should_resume_design_review,
    should_resume_doc_review,
    should_resume_duplicate_review,
    should_start_run,
    validate_webhook_signature,
)
from packages.integrations.queue import enqueue_process_run
from packages.orchestrator.graph import create_run, find_active_run

logger = structlog.get_logger(__name__)

app = FastAPI(title="Agent Nova", version="0.1.0")


@app.on_event("startup")
async def on_startup() -> None:
    settings = get_settings()
    logger.info(
        "api_started",
        jira_provider=settings.jira_provider,
        llm_provider=settings.llm_provider,
        app_env=settings.app_env,
    )
    await init_db()


@app.get("/health")
async def health() -> Dict[str, str]:
    return {"status": "ok"}


@app.get("/runs/{run_id}")
async def get_run(run_id: uuid.UUID, session: AsyncSession = Depends(get_db_session)) -> Dict[str, Any]:
    run = await session.get(Run, run_id)
    if run is None:
        raise HTTPException(status_code=404, detail="Run not found")
    return {
        "id": str(run.id),
        "jira_key": run.jira_key,
        "state": run.state.value,
        "retry_count": run.retry_count,
        "branch_name": run.branch_name,
        "pr_url": run.pr_url,
        "error": run.error,
        "artifact_dir": run.artifact_dir,
        "artifacts": run.artifacts,
    }


@app.post("/webhooks/jira")
async def jira_webhook(
    request: Request,
    session: AsyncSession = Depends(get_db_session),
    x_hub_signature: Optional[str] = Header(default=None, alias="X-Hub-Signature"),
    x_atlassian_webhook_identifier: Optional[str] = Header(
        default=None, alias="X-Atlassian-Webhook-Identifier"
    ),
) -> Dict[str, Any]:
    settings = get_settings()
    body = await request.body()
    if not validate_webhook_signature(body, x_hub_signature, settings.jira_webhook_secret):
        raise HTTPException(status_code=401, detail="Invalid webhook signature")

    try:
        payload = json.loads(body.decode("utf-8") or "{}")
    except json.JSONDecodeError as exc:
        raise HTTPException(status_code=400, detail="Invalid JSON") from exc

    delivery_id = (
        x_atlassian_webhook_identifier
        or request.headers.get("X-Delivery-Id")
        or hashlib.sha256(body).hexdigest()
    )

    existing = await session.execute(
        select(ProcessedWebhook).where(ProcessedWebhook.delivery_id == delivery_id)
    )
    if existing.scalar_one_or_none():
        return {"status": "duplicate", "delivery_id": delivery_id}

    event = normalize_jira_webhook(payload, delivery_id)
    session.add(
        ProcessedWebhook(
            delivery_id=delivery_id,
            jira_key=event.jira_key or None,
            event_type=event.event_type,
        )
    )
    await session.commit()

    if not event.jira_key:
        return {"status": "ignored", "reason": "missing_jira_key"}

    active = await find_active_run(session, event.jira_key)

    if should_resume_clarification(event, active.state if active else None, settings):
        assert active is not None
        logger.info("resume_clarification", jira_key=event.jira_key, run_id=str(active.id))
        try:
            await enqueue_process_run(str(active.id), resume=True)
        except Exception as exc:  # noqa: BLE001
            logger.warning("enqueue_failed_inline_fallback", error=str(exc))
            from packages.orchestrator.graph import Orchestrator

            orch = Orchestrator(session)
            await orch.run(active.id, resume=True)
        return {
            "status": "resumed",
            "run_id": str(active.id),
            "jira_key": event.jira_key,
        }

    if should_resume_duplicate_review(event, active.state if active else None, settings):
        assert active is not None
        decision = duplicate_review_decision(event) or "continue"
        arts = dict(active.artifacts or {})
        arts["duplicate_decision"] = decision
        arts["duplicate_resume_comment"] = event.comment_body
        active.artifacts = arts
        await session.commit()
        logger.info(
            "resume_duplicate_review",
            jira_key=event.jira_key,
            run_id=str(active.id),
            decision=decision,
        )
        try:
            await enqueue_process_run(str(active.id), resume=True)
        except Exception as exc:  # noqa: BLE001
            logger.warning("enqueue_failed_inline_fallback", error=str(exc))
            from packages.orchestrator.graph import Orchestrator

            orch = Orchestrator(session)
            await orch.run(active.id, resume=True)
        return {
            "status": "resumed",
            "run_id": str(active.id),
            "jira_key": event.jira_key,
            "decision": decision,
        }

    if should_resume_doc_review(event, active.state if active else None, settings):
        assert active is not None
        decision = doc_review_decision(event) or "continue"
        arts = dict(active.artifacts or {})
        arts["doc_decision"] = decision
        arts["doc_resume_comment"] = event.comment_body
        active.artifacts = arts
        await session.commit()
        logger.info(
            "resume_doc_review",
            jira_key=event.jira_key,
            run_id=str(active.id),
            decision=decision,
        )
        try:
            await enqueue_process_run(str(active.id), resume=True)
        except Exception as exc:  # noqa: BLE001
            logger.warning("enqueue_failed_inline_fallback", error=str(exc))
            from packages.orchestrator.graph import Orchestrator

            orch = Orchestrator(session)
            await orch.run(active.id, resume=True)
        return {
            "status": "resumed",
            "run_id": str(active.id),
            "jira_key": event.jira_key,
            "decision": decision,
        }

    if should_resume_design_review(event, active.state if active else None, settings):
        assert active is not None
        decision = design_review_decision(event)
        arts = dict(active.artifacts or {})
        arts["design_decision"] = decision
        arts["design_resume_comment"] = event.comment_body
        active.artifacts = arts
        await session.commit()
        logger.info(
            "resume_design_review",
            jira_key=event.jira_key,
            run_id=str(active.id),
            decision=decision,
        )
        try:
            await enqueue_process_run(str(active.id), resume=True)
        except Exception as exc:  # noqa: BLE001
            logger.warning("enqueue_failed_inline_fallback", error=str(exc))
            from packages.orchestrator.graph import Orchestrator

            orch = Orchestrator(session)
            await orch.run(active.id, resume=True)
        return {
            "status": "resumed",
            "run_id": str(active.id),
            "jira_key": event.jira_key,
            "decision": decision,
        }

    # Comment after approve while pipeline is still running (or already finished)
    if event.is_comment and active is not None and active.state != WorkflowState.NEEDS_CLARIFICATION:
        return {
            "status": "ignored",
            "reason": "already_in_progress",
            "run_id": str(active.id),
            "state": active.state.value,
        }

    if active is not None:
        return {
            "status": "ignored",
            "reason": "active_run_exists",
            "run_id": str(active.id),
            "state": active.state.value,
        }

    if event.is_comment:
        return {
            "status": "ignored",
            "reason": "no_resumable_run",
            "jira_key": event.jira_key,
            "hint": "No active run in NEEDS_CLARIFICATION, DUPLICATE_REVIEW, DOC_REVIEW, or DESIGN_REVIEW for this issue",
        }

    if not should_start_run(event, settings):
        return {"status": "ignored", "reason": "not_agent_ready"}

    run = await create_run(session, event.jira_key, idempotency_key=f"start:{delivery_id}")
    await _transition_jira_to_in_progress(session, run.id, event.jira_key, settings)
    try:
        await enqueue_process_run(str(run.id), resume=False)
    except Exception as exc:  # noqa: BLE001
        # Local/dev without Redis: process inline
        logger.warning("enqueue_failed_inline_fallback", error=str(exc))
        from packages.orchestrator.graph import Orchestrator

        orch = Orchestrator(session)
        await orch.run(run.id, resume=False)
    return {"status": "queued", "run_id": str(run.id), "jira_key": event.jira_key}


async def _transition_jira_to_in_progress(
    session: AsyncSession,
    run_id: uuid.UUID,
    jira_key: str,
    settings: Settings,
) -> None:
    """Best-effort board move to In Progress after a new run is created."""
    target = settings.jira_in_progress_status
    jira = get_jira_client(settings)
    try:
        moved = await jira.transition_issue(jira_key, target)
        if moved:
            session.add(
                AuditEvent(
                    run_id=run_id,
                    event_type="jira.transition",
                    message=f"Transitioned {jira_key} to {target}",
                    payload={"jira_key": jira_key, "status": target},
                )
            )
            logger.info("jira_transitioned", jira_key=jira_key, status=target)
        else:
            session.add(
                AuditEvent(
                    run_id=run_id,
                    event_type="jira.transition_failed",
                    message=f"No transition to {target} available for {jira_key}",
                    payload={"jira_key": jira_key, "status": target, "reason": "unavailable"},
                )
            )
            logger.warning(
                "jira_transition_unavailable",
                jira_key=jira_key,
                status=target,
            )
        await session.commit()
    except Exception as exc:  # noqa: BLE001
        logger.warning(
            "jira_transition_failed",
            jira_key=jira_key,
            status=target,
            error=str(exc),
        )
        try:
            session.add(
                AuditEvent(
                    run_id=run_id,
                    event_type="jira.transition_failed",
                    message=f"Failed to transition {jira_key} to {target}: {exc}",
                    payload={"jira_key": jira_key, "status": target, "error": str(exc)},
                )
            )
            await session.commit()
        except Exception:  # noqa: BLE001
            logger.exception("jira_transition_audit_failed", jira_key=jira_key)
