"""
BiasClear Standalone API

Nine endpoints.

  POST /scan                      — Text in, PIT distortion profile out.
  POST /scan/batch                — Batch scan up to 50 texts.
  POST /correct                   — Biased text in, clean text out with diff.
  GET  /patterns                  — Shows what the system detects.
  GET  /audit                     — SHA-256 audit chain provenance trail.
  GET  /health                    — Operational status check.
  POST /certificate               — Scan + generate verifiable HTML certificate.
  GET  /certificate/verify/{hash} — Verify certificate against audit chain.
  GET  /demo                      — Interactive playground.

Auth:
  Set BIASCLEAR_API_KEY env var to enable Bearer token auth on /scan and /correct.
  If unset, auth is disabled (development mode).

Boot:
  uvicorn biasclear.api:app --port 8100
"""

from __future__ import annotations

import logging
import os
import uuid
from fastapi import FastAPI, HTTPException, Depends, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel, Field
from typing import Optional

from .frozen_core import frozen_core, CORE_VERSION
from .detector import scan_local, scan_full
from .corrector import correct_bias
from .scorer import calculate_truth_score
from .audit import audit_chain
from .patterns.learned import learning_ring
from .llm.factory import get_provider


# ── Logging ──────────────────────────────────────────────────────────

_LOG_LEVEL = os.environ.get("BIASCLEAR_LOG_LEVEL", "INFO").upper()
logging.basicConfig(
    level=getattr(logging, _LOG_LEVEL, logging.INFO),
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)
log = logging.getLogger("biasclear.api")


# ── App ──────────────────────────────────────────────────────────────

app = FastAPI(
    title="BiasClear",
    description="Governed bias detection and correction engine",
    version=CORE_VERSION,
    docs_url="/docs",
    redoc_url="/redoc",
)

_CORS_ORIGINS = os.environ.get("BIASCLEAR_CORS_ORIGINS", "*").split(",")
app.add_middleware(
    CORSMiddleware,
    allow_origins=_CORS_ORIGINS,
    allow_methods=["*"],
    allow_headers=["*"],
)


# ── Auth ─────────────────────────────────────────────────────────────

_API_KEY = os.environ.get("BIASCLEAR_API_KEY")
_bearer_scheme = HTTPBearer(auto_error=False)


async def require_api_key(
    credentials: Optional[HTTPAuthorizationCredentials] = Depends(_bearer_scheme),
) -> None:
    """
    FastAPI dependency — enforces Bearer token auth on protected routes.

    If BIASCLEAR_API_KEY is not set, auth is disabled (dev mode).
    If set, every request to a protected route must include:
        Authorization: Bearer <key>
    """
    if _API_KEY is None:
        # Dev mode — no key configured, auth disabled
        return
    if credentials is None or credentials.credentials != _API_KEY:
        raise HTTPException(
            status_code=401,
            detail="Invalid or missing API key. Use Authorization: Bearer <key>.",
            headers={"WWW-Authenticate": "Bearer"},
        )


# ── Rate Limiting ────────────────────────────────────────────────────

import time
from collections import defaultdict

# Configurable limits (requests per minute)
SCAN_RATE_LIMIT = int(os.environ.get("BIASCLEAR_RATE_SCAN", "100"))
CORRECT_RATE_LIMIT = int(os.environ.get("BIASCLEAR_RATE_CORRECT", "20"))

# In-memory token bucket store: {(key, endpoint): [timestamps]}
_rate_store: dict[tuple[str, str], list[float]] = defaultdict(list)
_WINDOW = 60.0  # seconds


def _check_rate_limit(key: str, endpoint: str, limit: int) -> None:
    """
    Token bucket rate limiter.

    Tracks request timestamps per (key, endpoint) pair within
    a sliding 60-second window. Raises 429 if limit exceeded.
    """
    now = time.monotonic()
    bucket_key = (key, endpoint)

    # Prune expired entries
    _rate_store[bucket_key] = [
        t for t in _rate_store[bucket_key] if now - t < _WINDOW
    ]

    if len(_rate_store[bucket_key]) >= limit:
        # Calculate retry-after from oldest entry in window
        oldest = _rate_store[bucket_key][0]
        retry_after = int(_WINDOW - (now - oldest)) + 1
        raise HTTPException(
            status_code=429,
            detail=f"Rate limit exceeded: {limit} requests per minute. Try again in {retry_after}s.",
            headers={"Retry-After": str(retry_after)},
        )

    _rate_store[bucket_key].append(now)


async def require_scan_rate_limit(
    credentials: Optional[HTTPAuthorizationCredentials] = Depends(_bearer_scheme),
) -> None:
    """Rate limit for /scan endpoint."""
    key = credentials.credentials if credentials else "anonymous"
    _check_rate_limit(key, "scan", SCAN_RATE_LIMIT)


async def require_correct_rate_limit(
    credentials: Optional[HTTPAuthorizationCredentials] = Depends(_bearer_scheme),
) -> None:
    """Rate limit for /correct endpoint."""
    key = credentials.credentials if credentials else "anonymous"
    _check_rate_limit(key, "correct", CORRECT_RATE_LIMIT)


# ── Models ───────────────────────────────────────────────────────────

class ScanRequest(BaseModel):
    text: str = Field(..., min_length=1, description="Text to scan for bias distortions")
    domain: str = Field("general", description="Domain context: general | legal | media | financial")
    mode: str = Field("local", description="Scan mode: local (free, instant) | full (LLM-powered)")


class CorrectionRequest(BaseModel):
    text: str = Field(..., min_length=1, description="Biased text to correct")
    domain: str = Field("legal", description="Domain context")
    webhook_url: Optional[str] = Field(
        None,
        description=(
            "If set, correction runs async. Returns 202 with job_id "
            "immediately, POSTs result to this URL when done."
        ),
    )


class HealthResponse(BaseModel):
    status: str
    biasclear: str
    audit_entries: int


# ── Health ───────────────────────────────────────────────────────────

@app.get("/health", response_model=HealthResponse)
async def health():
    """Service health + version."""
    return HealthResponse(
        status="operational",
        biasclear=CORE_VERSION,
        audit_entries=audit_chain.get_count(),
    )


# ── POST /scan ───────────────────────────────────────────────────────

@app.post("/scan", dependencies=[Depends(require_api_key), Depends(require_scan_rate_limit)])
async def scan(req: ScanRequest):
    """
    Scan text for PIT distortions.

    Modes:
      - local:  Frozen core only. Zero cost. Deterministic. Instant.
      - full:   Frozen core + LLM deep analysis. Costs an API call.
    """
    try:
        if req.mode == "local":
            result = await scan_local(req.text, domain=req.domain)
        elif req.mode == "full":
            llm = get_provider()
            result = await scan_full(
                req.text,
                llm=llm,
                domain=req.domain,
                learning_ring=learning_ring,
                audit_chain=audit_chain,
            )
        else:
            raise HTTPException(
                status_code=400,
                detail=f"Invalid scan mode: {req.mode}. Use 'local' or 'full'.",
            )

        # Log the scan
        audit_chain.log(
            event_type=f"scan_{req.mode}",
            data={
                "domain": req.domain,
                "truth_score": result.get("truth_score"),
                "flag_count": result.get("flag_count", 0),
                "text_length": len(req.text),
            },
            core_version=CORE_VERSION,
        )

        return result

    except HTTPException:
        raise
    except Exception as e:
        log.exception("Scan failed")
        raise HTTPException(status_code=500, detail=f"Scan failed: {str(e)}")


# ── POST /scan/batch ─────────────────────────────────────────────────

class BatchScanRequest(BaseModel):
    texts: list[str] = Field(..., min_length=1, max_length=50, description="List of texts to scan (max 50)")
    domain: str = Field("general", description="Domain context")
    mode: str = Field("local", description="Scan mode: local | full")


@app.post("/scan/batch", dependencies=[Depends(require_api_key), Depends(require_scan_rate_limit)])
async def scan_batch(req: BatchScanRequest):
    """
    Batch scan multiple texts in one request.

    Returns a list of scan results, one per input text.
    Max 50 texts per request.
    """
    results = []
    for i, text in enumerate(req.texts):
        try:
            if not text.strip():
                results.append({"index": i, "error": "Empty text", "skipped": True})
                continue

            if req.mode == "local":
                result = await scan_local(text, domain=req.domain)
            elif req.mode == "full":
                llm = get_provider()
                result = await scan_full(
                    text, llm=llm, domain=req.domain,
                    learning_ring=learning_ring, audit_chain=audit_chain,
                )
            else:
                raise HTTPException(status_code=400, detail=f"Invalid mode: {req.mode}")

            result["index"] = i
            results.append(result)

        except HTTPException:
            raise
        except Exception as e:
            log.warning("Batch scan item %d failed: %s", i, e)
            results.append({"index": i, "error": str(e), "skipped": True})

    # Aggregate stats
    scanned = [r for r in results if not r.get("skipped")]
    flagged = [r for r in scanned if r.get("bias_detected")]
    avg_score = sum(r.get("truth_score", 0) for r in scanned) / max(len(scanned), 1)

    audit_chain.log(
        event_type="scan_batch",
        data={
            "count": len(req.texts),
            "scanned": len(scanned),
            "flagged": len(flagged),
            "avg_truth_score": round(avg_score, 1),
            "domain": req.domain,
        },
        core_version=CORE_VERSION,
    )

    return {
        "results": results,
        "summary": {
            "total": len(req.texts),
            "scanned": len(scanned),
            "flagged": len(flagged),
            "clean": len(scanned) - len(flagged),
            "skipped": len(req.texts) - len(scanned),
            "avg_truth_score": round(avg_score, 1),
        },
    }


# ── POST /correct ────────────────────────────────────────────────────

@app.post("/correct", dependencies=[Depends(require_api_key), Depends(require_correct_rate_limit)])
async def correct(req: CorrectionRequest, background_tasks: BackgroundTasks):
    """
    Correct bias in text.

    Runs a full scan, then iterative LLM correction with
    post-correction verification. Returns corrected text with
    deterministic diff spans showing what changed.

    If webhook_url is provided, returns 202 immediately and
    POSTs the result to the webhook when done.
    """
    # -- Async / webhook mode --
    if req.webhook_url:
        job_id = str(uuid.uuid4())
        background_tasks.add_task(
            _run_correction_and_notify,
            job_id=job_id,
            text=req.text,
            domain=req.domain,
            webhook_url=req.webhook_url,
        )
        return {"job_id": job_id, "status": "accepted", "webhook_url": req.webhook_url}

    # -- Synchronous mode (default) --
    return await _run_correction(req.text, req.domain)


async def _run_correction(text: str, domain: str) -> dict:
    """Execute correction pipeline and return result dict."""
    try:
        llm = get_provider()
        scan_result = await scan_full(
            text,
            llm=llm,
            domain=domain,
            learning_ring=learning_ring,
            audit_chain=audit_chain,
        )
        correction = await correct_bias(
            text, scan_result, llm=llm, domain=domain
        )
        audit_chain.log(
            event_type="correction",
            data={
                "domain": domain,
                "corrected": correction.get("corrected", False),
                "truth_score_before": correction.get("truth_score_before"),
                "truth_score_after": correction.get("truth_score_after"),
                "text_length": len(text),
            },
            core_version=CORE_VERSION,
        )
        return {
            "corrected": correction.get("corrected", False),
            "original_text": text,
            "corrected_text": correction.get("corrected_text"),
            "truth_score_before": correction.get("truth_score_before"),
            "truth_score_after": correction.get("truth_score_after"),
            "diff_spans": correction.get("diff_spans"),
            "changes_made": correction.get("changes_made"),
            "iterations": correction.get("iterations"),
            "reason": correction.get("reason"),
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Correction failed: {str(e)}")


async def _run_correction_and_notify(
    job_id: str, text: str, domain: str, webhook_url: str
) -> None:
    """
    Background task: run correction, POST result to webhook.

    Sends JSON payload with job_id, status, and full correction result.
    On failure, sends error payload to webhook.
    """
    import httpx as _httpx

    try:
        result = await _run_correction(text, domain)
        payload = {"job_id": job_id, "status": "completed", "result": result}
    except HTTPException as e:
        payload = {"job_id": job_id, "status": "failed", "error": e.detail}
    except Exception as e:
        payload = {"job_id": job_id, "status": "failed", "error": str(e)}

    try:
        async with _httpx.AsyncClient(timeout=30.0) as client:
            await client.post(webhook_url, json=payload)
    except Exception:
        # Webhook delivery failure is non-fatal — logged in audit
        audit_chain.log(
            event_type="webhook_delivery_failed",
            data={"job_id": job_id, "webhook_url": webhook_url},
            core_version=CORE_VERSION,
        )


# ── GET /patterns ────────────────────────────────────────────────────

@app.get("/patterns")
async def get_patterns(domain: str = "general"):
    """
    Return the active detection surface.

    Shows all frozen structural patterns + any active learned patterns.
    This is the transparency endpoint — shows users exactly what
    the system detects and how the learning ring has evolved.
    """
    # Frozen patterns
    frozen = frozen_core.get_patterns(domain=domain)

    # Learned patterns (active only)
    learned_active = learning_ring.get_active_patterns()

    # Learning ring stats
    ring_stats = learning_ring.get_stats()

    return {
        "domain": domain,
        "core_version": CORE_VERSION,
        "frozen_patterns": frozen,
        "learned_patterns": [
            {
                "pattern_id": p.id,
                "name": p.name,
                "description": p.description,
                "pit_tier": p.pit_tier,
                "severity": p.severity,
                "source": "learning_ring",
            }
            for p in learned_active
        ],
        "frozen_count": len(frozen),
        "learned_count": len(learned_active),
        "learning_ring": ring_stats,
    }


# ── GET /audit ───────────────────────────────────────────────────────

@app.get("/audit")
async def get_audit(limit: int = 20, event_type: Optional[str] = None):
    """
    Return recent audit chain entries with chain integrity verification.

    Provenance endpoint — proves what the system did, when, and how.
    Non-negotiable for enterprise and legal customers.
    """
    entries = audit_chain.get_recent(limit=limit, event_type=event_type)
    integrity = audit_chain.verify_chain(limit=limit)

    return {
        "entries": entries,
        "count": len(entries),
        "total_entries": audit_chain.get_count(),
        "chain_integrity": integrity,
        "core_version": CORE_VERSION,
    }



# ── POST /certificate ────────────────────────────────────────────────

from datetime import datetime, timezone
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
from .certificate import generate_certificate_html, compute_certificate_id


class CertificateRequest(BaseModel):
    text: str = Field(..., min_length=1, description="Text to scan and certify")
    domain: str = Field("general", description="Domain context: general | legal | media | financial")
    mode: str = Field("local", description="Scan mode: local (instant) | full (LLM-powered)")
    format: str = Field("html", description="Response format: html | json")


@app.post("/certificate", dependencies=[Depends(require_api_key), Depends(require_scan_rate_limit)])
async def create_certificate(req: CertificateRequest):
    """
    Scan text and generate a verifiable bias certificate.

    Runs the full scan pipeline, logs to the audit chain, and returns
    a self-contained HTML certificate with the SHA-256 audit hash.

    The certificate is independently verifiable via GET /certificate/verify/{hash}.
    """
    try:
        # Run the scan
        if req.mode == "local":
            scan_result = await scan_local(req.text, domain=req.domain)
        elif req.mode == "full":
            llm = get_provider()
            scan_result = await scan_full(
                req.text, llm=llm, domain=req.domain,
                learning_ring=learning_ring, audit_chain=audit_chain,
            )
        else:
            raise HTTPException(status_code=400, detail=f"Invalid mode: {req.mode}")

        # Generate certificate metadata
        issued_at = datetime.now(timezone.utc).isoformat()
        certificate_id = compute_certificate_id(req.text, issued_at)

        # Log to audit chain with certificate event type
        audit_hash = audit_chain.log(
            event_type="certificate_issued",
            data={
                "certificate_id": certificate_id,
                "domain": req.domain,
                "mode": req.mode,
                "truth_score": scan_result.get("truth_score"),
                "flag_count": scan_result.get("flag_count", 0),
                "bias_detected": scan_result.get("bias_detected", False),
                "text_length": len(req.text),
                "text_hash": compute_certificate_id(req.text, ""),
            },
            core_version=CORE_VERSION,
        )

        # Determine verify URL
        base_url = os.environ.get("BIASCLEAR_BASE_URL", "http://localhost:8100")
        verify_url = f"{base_url}/certificate/verify/{audit_hash}"

        if req.format == "json":
            return {
                "certificate_id": certificate_id,
                "audit_hash": audit_hash,
                "verify_url": verify_url,
                "issued_at": issued_at,
                "scan_result": scan_result,
            }

        # Generate HTML certificate
        html = generate_certificate_html(
            text=req.text,
            scan_result=scan_result,
            audit_hash=audit_hash,
            certificate_id=certificate_id,
            issued_at=issued_at,
            verify_url=verify_url,
        )

        return HTMLResponse(content=html, status_code=200)

    except HTTPException:
        raise
    except Exception as e:
        log.exception("Certificate generation failed")
        raise HTTPException(status_code=500, detail=f"Certificate failed: {str(e)}")


# ── GET /certificate/verify ──────────────────────────────────────────

@app.get("/certificate/verify/{audit_hash}")
async def verify_certificate(audit_hash: str):
    """
    Verify a BiasClear scan certificate against the audit chain.

    Returns the original audit entry and chain integrity status.
    This endpoint is public — no auth required.
    """
    # Find the entry in the audit chain
    entries = audit_chain.get_recent(limit=1000, event_type="certificate_issued")
    target = None
    for entry in entries:
        if entry["hash"] == audit_hash:
            target = entry
            break

    if not target:
        raise HTTPException(
            status_code=404,
            detail="Certificate not found. The hash may be invalid or the audit chain may have been rotated.",
        )

    # Verify chain integrity around this entry
    integrity = audit_chain.verify_chain(limit=1000)

    return {
        "verified": True,
        "certificate": {
            "audit_hash": target["hash"],
            "issued_at": target["timestamp"],
            "event_type": target["event_type"],
            "data": target["data"],
            "core_version": target["core_version"],
        },
        "chain_integrity": {
            "verified": integrity["verified"],
            "entries_checked": integrity["entries_checked"],
            "tampered": not integrity["verified"],
        },
    }


# ── Demo Page ────────────────────────────────────────────────────────

from pathlib import Path

_DEMO_HTML = Path(__file__).parent / "demo.html"


@app.get("/demo", response_class=HTMLResponse, include_in_schema=False)
async def demo():
    """Serve the interactive demo page."""
    if _DEMO_HTML.exists():
        return HTMLResponse(content=_DEMO_HTML.read_text(), status_code=200)
    raise HTTPException(status_code=404, detail="Demo page not found")


@app.get("/", include_in_schema=False)
async def root():
    """Redirect root to demo page."""
    return RedirectResponse(url="/demo")

