from __future__ import annotations

import uuid
from typing import cast

from fastapi import APIRouter
from fastapi.responses import JSONResponse, PlainTextResponse
from grantflow.api.errors import raise_agent_error

from grantflow.api.bid_no_bid import CRITERIA_ORDER, evaluate_bid_no_bid
from grantflow.api.diagnostics_service import _health_diagnostics
from grantflow.api.job_store_service import _get_job, _set_job
from grantflow.api.pipeline_jobs import _run_pipeline_to_completion
from grantflow.api.public_views import public_job_events_payload, public_job_quality_payload
from grantflow.api.readiness_service import _build_readiness_payload
from grantflow.api.schemas import (
    BidNoBidRequest,
    BidNoBidResponse,
    BidNoBidSimulationRequest,
    BidNoBidSimulationResponse,
)
from grantflow.core.strategies.factory import DonorFactory
from grantflow.core.version import __version__
from grantflow.swarm.state_contract import build_graph_state

system_router = APIRouter(tags=["system"])

_LLMS_TXT_PATH = None  # resolved lazily


def _llms_txt_content() -> str:
    """Return the llms.txt content, served both as a file and at GET /llms.txt."""
    return """\
# GrantFlow

> Agent-native grant workflow API for governed proposal operations.

GrantFlow is an HTTP API and MCP tool server that AI agents use to run governed grant proposal workflows: preflight checks, deterministic generation, human-in-the-loop review, quality inspection, and export-ready evidence packs. It is not a chatbot or a CRM.

## Quickstart for agents

- Discover: `GET /.well-known/agent-capabilities.json`
- Tool manifest: `GET /.well-known/agent-tools.json`
- Recipes: `GET /.well-known/agent-recipes.json`
- Live demo (no auth): `GET /demo/run`
- Register sandbox identity: `POST /agents/register`
- Onboard (api_key or oauth_client_credentials): `POST /agents/onboarding`
- Token exchange: `POST /agents/oauth/token`
- Credential introspect: `POST /agents/introspect`

## Core workflow

1. `POST /agents/onboarding` — get API key or OAuth client
2. `POST /ingest/text` — load reference documents (requires ingest:write scope)
3. `POST /ingest/readiness` — confirm corpus coverage
4. `POST /generate/preflight` — check donor readiness before generation
5. `POST /generate` — start a job (returns job_id, poll or use webhook)
6. `GET /status/{job_id}` — poll until done or pending_hitl
7. `POST /hitl/approve` — unblock pending_hitl with approved=true
8. `GET /status/{job_id}/quality` — trust_summary, grounding, critic verdict
9. `GET /status/{job_id}/export-payload` — structured pre-export data
10. `POST /agents/credentials/revoke` — revoke session after work

## Tenant and usage

- Register tenant: `POST /tenants`
- List tenants: `GET /tenants`
- Tenant detail + usage: `GET /tenants/{tenant_id}`
- Usage only: `GET /tenants/{tenant_id}/usage`

## Authentication

Send `X-API-Key: <key>` or `Authorization: Bearer <token>` on protected endpoints.
Auth is optional in sandbox mode. Set `GRANTFLOW_API_KEY` on the server to enforce it.

## Error contract

All errors: `{"detail": {"code": "...", "message": "...", "retryable": bool, "next_action": "..."}}`
`retryable: true` = safe to retry after a wait. `retryable: false` = fix the request first.

## MCP tools

Run: `python -m grantflow.mcp.server`
Key tools: grantflow_onboard_agent, grantflow_ingest_text, grantflow_run_preflight,
grantflow_start_generation, grantflow_get_status, grantflow_hitl_approve,
grantflow_get_quality, grantflow_get_export_payload, grantflow_run_sandbox_happy_path

## Supported donors

usaid, eu, worldbank, giz, state_department

## Source and docs

- AGENTS.md — canonical agent integration guide
- docs/agents/quickstart.md — human quickstart
- docs/agents/error-contract.md — all error codes
- docs/agents/trust-report.md — what GrantFlow verifies vs. operator responsibility
"""


@system_router.get("/demo/run", tags=["system"])
def run_demo():
    """Sandbox smoke run — no auth required.

    Executes a deterministic non-LLM generate cycle for USAID and returns
    a structured summary. Use this to verify the API is working end-to-end
    before onboarding or integrating.

    Typically completes in under 2 seconds.
    """
    donor_id = "usaid"
    try:
        strategy = DonorFactory.get_strategy(donor_id)
    except Exception as exc:
        raise_agent_error(
            status_code=503,
            code="demo_unavailable",
            message=f"Demo unavailable: {exc}",
            retryable=True,
            next_action="Retry in a few seconds. If the issue persists, check server logs.",
        )

    job_id = f"demo_{uuid.uuid4().hex[:12]}"
    initial_state = build_graph_state(
        donor_id=donor_id,
        input_context={
            "project": "Digital Governance Pilot",
            "country": "Kazakhstan",
            "sector": "digital_governance",
        },
        donor_strategy=strategy,
        tenant_id="demo_sandbox",
        llm_mode=False,
        hitl_checkpoints=[],
        max_iterations=1,
    )

    _set_job(
        job_id,
        {
            "status": "accepted",
            "state": initial_state,
            "hitl_enabled": False,
            "client_metadata": {"demo": True, "tenant_id": "demo_sandbox"},
        },
    )

    try:
        _run_pipeline_to_completion(job_id, cast(dict, initial_state))
    except Exception as exc:
        raise_agent_error(
            status_code=500,
            code="demo_pipeline_error",
            message=f"Demo pipeline error: {exc}",
            retryable=True,
            next_action="Retry the demo endpoint. If the issue persists, check server logs.",
        )

    job = _get_job(job_id)
    if not job:
        raise_agent_error(
            status_code=500,
            code="internal_error",
            message="Demo job was not found after the run completed.",
            retryable=False,
            next_action="Retry the demo endpoint. If the issue persists, check server logs.",
        )

    status = str(job.get("status") or "")
    events_payload = public_job_events_payload(job_id, job)
    quality_payload = public_job_quality_payload(job_id, job)

    payload = {
        "demo": True,
        "job_id": job_id,
        "donor_id": donor_id,
        "status": status,
        "event_count": events_payload.get("event_count", 0),
        "quality": {
            "verdict": (quality_payload.get("trust_summary") or {}).get("verdict"),
            "grounding_verified": (quality_payload.get("trust_summary") or {}).get("grounding_verified"),
            "critic_score": quality_payload.get("critic_score"),
        },
        "note": (
            "This is a deterministic sandbox run (llm_mode=false). "
            "Real proposals require a corpus via POST /ingest/text and llm_mode=true."
        ),
        "next": {
            "onboard": "POST /agents/onboarding",
            "ingest": "POST /ingest/text",
            "preflight": "POST /generate/preflight",
            "generate": "POST /generate",
        },
    }
    return JSONResponse(
        content=payload,
        headers={
            "x-agent-sandbox": "true",
            "x-auth-required": "false",
            "x-grantflow-version": __version__,
        },
    )


@system_router.get("/llms.txt", response_class=PlainTextResponse, tags=["system"])
def serve_llms_txt():
    """Machine-readable project description for LLM agents (llms.txt standard)."""
    return _llms_txt_content()


@system_router.get("/", tags=["system"])
def root_index():
    """Service index: pointers to demo, health, docs, and agent discovery."""
    return {
        "name": "GrantFlow",
        "version": __version__,
        "description": (
            "Agent-native API for governed grant-proposal workflows. "
            "Human review required. Not legal, compliance, sanctions, "
            "financial, or trading advice."
        ),
        "links": {
            "demo": "/demo/run",
            "health": "/health",
            "ready": "/ready",
            "docs": "/docs",
            "openapi": "/openapi.json",
            "llms_txt": "/llms.txt",
            "agent_descriptor": "/.well-known/agent.json",
            "capabilities": "/.well-known/agent-capabilities.json",
        },
    }


@system_router.get("/health")
def health_check():
    return {
        "status": "healthy",
        "version": __version__,
        "diagnostics": _health_diagnostics(),
    }


@system_router.get("/ready")
def readiness_check():
    payload = _build_readiness_payload()
    if str(payload.get("status") or "").strip().lower() != "ready":
        raise_agent_error(
            status_code=503,
            code="service_not_ready",
            message="Service is not ready.",
            retryable=True,
            next_action="Wait for dependencies to become available and retry.",
            **payload,
        )
    return payload


def _extract_bid_no_bid_scores(payload: BidNoBidRequest) -> dict[str, int]:
    return {
        "strategic_fit": payload.strategic_fit,
        "win_probability": payload.win_probability,
        "budget_margin": payload.budget_margin,
        "delivery_capacity": payload.delivery_capacity,
        "compliance_readiness": payload.compliance_readiness,
        "partner_strength": payload.partner_strength,
        "timeline_realism": payload.timeline_realism,
        "evidence_strength": payload.evidence_strength,
    }


@system_router.post("/decision/bid-no-bid", response_model=BidNoBidResponse)
def bid_no_bid_decision(payload: BidNoBidRequest):
    scores = _extract_bid_no_bid_scores(payload)

    invalid_fields = [key for key, value in scores.items() if int(value) < 0 or int(value) > 100]
    if invalid_fields:
        raise_agent_error(
            status_code=400,
            code="invalid_parameter",
            message=f"Scores must be between 0 and 100: {', '.join(invalid_fields)}",
            retryable=False,
            next_action="Set all score fields to integer values between 0 and 100.",
            invalid_fields=invalid_fields,
        )

    return evaluate_bid_no_bid(
        scores=scores,
        donor_profile=payload.donor_profile,
        weight_overrides=payload.weight_overrides,
        mandatory_eligibility_gap=payload.mandatory_eligibility_gap,
        conflict_of_interest=payload.conflict_of_interest,
    )


@system_router.post("/decision/bid-no-bid/simulate", response_model=BidNoBidSimulationResponse)
def bid_no_bid_simulation(payload: BidNoBidSimulationRequest):
    scores = _extract_bid_no_bid_scores(payload)

    invalid_fields = [key for key, value in scores.items() if int(value) < 0 or int(value) > 100]
    if invalid_fields:
        raise_agent_error(
            status_code=400,
            code="invalid_parameter",
            message=f"Scores must be between 0 and 100: {', '.join(invalid_fields)}",
            retryable=False,
            next_action="Set all score fields to integer values between 0 and 100.",
            invalid_fields=invalid_fields,
        )

    step = int(payload.step)
    if step < 1 or step > 30:
        raise_agent_error(
            status_code=400,
            code="invalid_parameter",
            message="step must be between 1 and 30.",
            retryable=False,
            next_action="Provide an integer value in the range 1–30.",
        )

    max_scenarios = int(payload.max_scenarios)
    if max_scenarios < 1 or max_scenarios > 8:
        raise_agent_error(
            status_code=400,
            code="invalid_parameter",
            message="max_scenarios must be between 1 and 8.",
            retryable=False,
            next_action="Provide an integer value in the range 1–8.",
        )

    baseline = evaluate_bid_no_bid(
        scores=scores,
        donor_profile=payload.donor_profile,
        weight_overrides=payload.weight_overrides,
        mandatory_eligibility_gap=payload.mandatory_eligibility_gap,
        conflict_of_interest=payload.conflict_of_interest,
    )

    ranked: list[tuple[float, bool, dict[str, object]]] = []
    for criterion in CRITERIA_ORDER:
        current = int(scores.get(criterion, 0))
        if current >= 100:
            continue
        simulated_scores = dict(scores)
        simulated_scores[criterion] = min(100, current + step)
        simulated = evaluate_bid_no_bid(
            scores=simulated_scores,
            donor_profile=payload.donor_profile,
            weight_overrides=payload.weight_overrides,
            mandatory_eligibility_gap=payload.mandatory_eligibility_gap,
            conflict_of_interest=payload.conflict_of_interest,
        )
        delta = round(float(simulated["weighted_score"]) - float(baseline["weighted_score"]), 2)
        scenario = {
            "criterion": criterion,
            "from_score": current,
            "to_score": simulated_scores[criterion],
            "score_delta": delta,
            "verdict": simulated["verdict"],
            "projected_weighted_score": simulated["weighted_score"],
        }
        ranked.append((delta, str(simulated["verdict"]) == "BID", scenario))

    ranked.sort(key=lambda row: (row[0], row[1]), reverse=True)
    candidates = [row[2] for row in ranked]
    return {
        "baseline": baseline,
        "scenarios": candidates[:max_scenarios],
    }
