# FILE: apps/svc-coverage/main.py

# Coverage policy service with evaluation, clarification, and hot reload

import os
import sys
from typing import Any

import structlog
from fastapi import Depends, HTTPException
from pydantic import BaseModel

sys.path.append(os.path.join(os.path.dirname(__file__), "..", ".."))

from libs.app_factory import create_app
from libs.config import BaseAppSettings, create_event_bus, create_neo4j_client
from libs.coverage import CoverageEvaluator
from libs.events import EventBus
from libs.neo import Neo4jClient
from libs.observability import get_metrics, get_tracer, setup_observability
from libs.policy import PolicyLoader, get_policy_loader
from libs.schemas import (
    ClarifyContext,
    ClarifyResponse,
    CoverageGap,
    CoverageReport,
    PolicyError,
    UploadOption,
    ValidationResult,
)
from libs.security import get_current_user, get_tenant_id

logger = structlog.get_logger()


async def http_exception_handler(_request, exc) -> dict[str, str | int]:
    """Handle HTTP exceptions"""
    return {"detail": exc.detail, "status_code": exc.status_code}


class CoverageSettings(BaseAppSettings):
    """Settings for Coverage service"""

    service_name: str = "svc-coverage"

    # Policy configuration
    config_dir: str = "config"
    policy_reload_enabled: bool = True

    # Database
    postgres_url: str = "postgresql://user:pass@localhost:5432/coverage"

    # External services
    rag_service_url: str = "http://svc-rag-retriever:8000"


# Create app and settings
app, settings = create_app(
    service_name="svc-coverage",
    title="Tax Agent Coverage Policy Service",
    description="Coverage policy evaluation and clarification service",
    settings_class=CoverageSettings,
)

# Global state
neo4j_client: Neo4jClient | None = None
event_bus: EventBus | None = None
policy_loader: PolicyLoader | None = None
current_policy: Any = None


@app.on_event("startup")
async def startup_event() -> None:
    """Initialize service dependencies"""
    global neo4j_client, event_bus, policy_loader, current_policy

    # Setup observability
    setup_observability(settings)

    # Initialize Neo4j client
    neo4j_driver = create_neo4j_client(settings)
    neo4j_client = Neo4jClient(neo4j_driver)

    # Initialize event bus
    event_bus = create_event_bus(settings)

    # Initialize policy loader
    policy_loader = get_policy_loader(settings.config_dir)

    # Load initial policy
    try:
        policy = policy_loader.load_policy()
        current_policy = policy_loader.compile_predicates(policy)
        logger.info("Initial policy loaded", version=policy.version)
    except Exception as e:
        logger.error("Failed to load initial policy", error=str(e))
        current_policy = None

    logger.info("Coverage service started")


@app.on_event("shutdown")
async def shutdown_event() -> None:
    """Cleanup service dependencies"""
    global neo4j_client, event_bus

    if neo4j_client:
        await neo4j_client.close()

    if event_bus:
        await event_bus.close()

    logger.info("Coverage service stopped")


# Request/Response models
class CheckCoverageRequest(BaseModel):
    """Request to check document coverage"""

    tax_year: str
    taxpayer_id: str


class ClarifyRequest(BaseModel):
    """Request to generate clarifying question"""

    gap: CoverageGap
    context: ClarifyContext


class ReloadRequest(BaseModel):
    """Request to reload policy"""

    force: bool = False


# Metrics
metrics = get_metrics()
tracer = get_tracer()


@app.post("/v1/coverage/check")
async def check_coverage(
    request: CheckCoverageRequest,
    current_user: dict[str, Any] = Depends(get_current_user),
    tenant_id: str = Depends(get_tenant_id),
) -> CoverageReport:
    """Check document coverage for taxpayer"""

    with tracer.start_as_current_span("check_coverage") as span:
        span.set_attribute("taxpayer_id", request.taxpayer_id)
        span.set_attribute("tax_year", request.tax_year)
        span.set_attribute("tenant_id", tenant_id)

        try:
            if not current_policy:
                raise HTTPException(status_code=503, detail="Policy not loaded")

            # Create evaluator with KG and RAG clients
            evaluator = CoverageEvaluator(
                kg_client=neo4j_client,
                rag_client=None,  # TODO: Initialize RAG client
            )

            # Perform coverage evaluation
            report = await evaluator.check_document_coverage(
                request.taxpayer_id,
                request.tax_year,
                current_policy,
            )

            # Record audit trail
            await _record_coverage_audit(report, tenant_id)

            # Update metrics
            metrics.counter("coverage_checks_total").labels(
                tenant_id=tenant_id,
                tax_year=request.tax_year,
                overall_status=report.overall_status.value,
            ).inc()

            return report

        except HTTPException:
            # Re-raise HTTP exceptions as-is
            raise
        except Exception as e:
            logger.error(
                "Coverage check failed",
                taxpayer_id=request.taxpayer_id,
                tax_year=request.tax_year,
                error=str(e),
            )
            raise HTTPException(
                status_code=500, detail=f"Coverage check failed: {str(e)}"
            ) from e


@app.post("/v1/coverage/clarify")
async def clarify_gap(
    request: ClarifyRequest,
    current_user: dict[str, Any] = Depends(get_current_user),
    tenant_id: str = Depends(get_tenant_id),
) -> ClarifyResponse:
    """Generate clarifying question for coverage gap"""

    with tracer.start_as_current_span("clarify_gap") as span:
        span.set_attribute("schedule_id", request.gap.schedule_id)
        span.set_attribute("evidence_id", request.gap.evidence_id)
        span.set_attribute("tenant_id", tenant_id)

        try:
            if not current_policy:
                raise HTTPException(status_code=503, detail="Policy not loaded")

            # Generate clarifying question
            response = await _generate_clarifying_question(request.gap, request.context)

            # Update metrics
            metrics.counter("clarifications_total").labels(
                tenant_id=tenant_id,
                schedule_id=request.gap.schedule_id,
                evidence_id=request.gap.evidence_id,
            ).inc()

            return response

        except HTTPException:
            # Re-raise HTTP exceptions as-is
            raise
        except Exception as e:
            logger.error(
                "Clarification failed",
                gap=request.gap.dict(),
                error=str(e),
            )
            raise HTTPException(
                status_code=500, detail=f"Clarification failed: {str(e)}"
            ) from e


@app.post("/admin/coverage/reload")
async def reload_policy(
    request: ReloadRequest,
    current_user: dict[str, Any] = Depends(get_current_user),
    tenant_id: str = Depends(get_tenant_id),
) -> dict[str, Any]:
    """Reload coverage policy from files"""

    # Check admin permissions
    user_groups = current_user.get("groups", [])
    if "admin" not in user_groups:
        raise HTTPException(status_code=403, detail="Admin access required")

    with tracer.start_as_current_span("reload_policy") as span:
        span.set_attribute("tenant_id", tenant_id)
        span.set_attribute("force", request.force)

        try:
            global current_policy

            if not policy_loader:
                raise HTTPException(
                    status_code=503, detail="Policy loader not initialized"
                )

            # Load and compile new policy
            policy = policy_loader.load_policy()
            new_compiled_policy = policy_loader.compile_predicates(policy)

            # Record new policy version
            await _record_policy_version(new_compiled_policy, tenant_id)

            # Update current policy
            current_policy = new_compiled_policy

            logger.info(
                "Policy reloaded",
                version=policy.version,
                hash=new_compiled_policy.hash,
                tenant_id=tenant_id,
            )

            return {
                "success": True,
                "version": policy.version,
                "hash": new_compiled_policy.hash,
                "compiled_at": new_compiled_policy.compiled_at.isoformat(),
                "source_files": new_compiled_policy.source_files,
            }

        except PolicyError as e:
            logger.error("Policy reload failed", error=str(e))
            raise HTTPException(
                status_code=400, detail=f"Policy error: {str(e)}"
            ) from e
        except Exception as e:
            logger.error("Policy reload failed", error=str(e))
            raise HTTPException(
                status_code=500, detail=f"Reload failed: {str(e)}"
            ) from e


@app.get("/v1/coverage/policy")
async def get_current_policy(
    current_user: dict[str, Any] = Depends(get_current_user),
    tenant_id: str = Depends(get_tenant_id),
) -> dict[str, Any]:
    """Get current compiled policy (no secrets, no PII)"""

    with tracer.start_as_current_span("get_policy") as span:
        span.set_attribute("tenant_id", tenant_id)

        if not current_policy:
            raise HTTPException(status_code=503, detail="Policy not loaded")

        # Return sanitized policy info
        return {
            "version": current_policy.policy.version,
            "jurisdiction": current_policy.policy.jurisdiction,
            "tax_year": current_policy.policy.tax_year,
            "compiled_at": current_policy.compiled_at.isoformat(),
            "hash": current_policy.hash,
            "source_files": current_policy.source_files,
            "schedules": list(current_policy.policy.schedules.keys()),
            "document_kinds": current_policy.policy.document_kinds,
        }


@app.get("/v1/coverage/validate")
async def validate_policy(
    current_user: dict[str, Any] = Depends(get_current_user),
    tenant_id: str = Depends(get_tenant_id),
) -> ValidationResult:
    """Validate current policy configuration"""

    with tracer.start_as_current_span("validate_policy") as span:
        span.set_attribute("tenant_id", tenant_id)

        try:
            if not policy_loader:
                raise HTTPException(
                    status_code=503, detail="Policy loader not initialized"
                )

            # Load policy as dict for validation
            policy_dict = policy_loader._load_yaml_file(
                os.path.join(settings.config_dir, "coverage.yaml")
            )

            # Validate policy
            result = policy_loader.validate_policy(policy_dict)

            # Additional validation: check box existence in KG
            if neo4j_client and result.ok:
                box_validation_errors = await _validate_boxes_in_kg(policy_dict)
                if box_validation_errors:
                    result.errors.extend(box_validation_errors)
                    result.ok = False

            return result

        except Exception as e:
            logger.error("Policy validation failed", error=str(e))
            return ValidationResult(
                ok=False,
                errors=[f"Validation failed: {str(e)}"],
            )


# Helper functions


async def _record_coverage_audit(report: CoverageReport, tenant_id: str) -> None:
    """Record coverage audit trail"""
    # TODO: Implement database recording
    logger.info(
        "Coverage audit recorded",
        taxpayer_id=report.taxpayer_id,
        tax_year=report.tax_year,
        overall_status=report.overall_status.value,
        blocking_items=len(report.blocking_items),
        tenant_id=tenant_id,
    )


async def _record_policy_version(compiled_policy: Any, tenant_id: str) -> None:
    """Record new policy version"""
    # TODO: Implement database recording
    logger.info(
        "Policy version recorded",
        version=compiled_policy.policy.version,
        hash=compiled_policy.hash,
        tenant_id=tenant_id,
    )


async def _generate_clarifying_question(
    gap: CoverageGap, context: ClarifyContext
) -> ClarifyResponse:
    """Generate clarifying question for coverage gap"""

    if not current_policy:
        raise ValueError("Policy not loaded")

    # Get question template
    templates = current_policy.policy.question_templates
    default_template = templates.default

    # Build question text
    evidence_name = gap.evidence_id
    schedule_name = gap.schedule_id
    boxes_text = ", ".join(gap.boxes) if gap.boxes else "relevant boxes"
    alternatives_text = (
        ", ".join(gap.acceptable_alternatives)
        if gap.acceptable_alternatives
        else "alternative documents"
    )

    question_text = default_template["text"].format(
        schedule=schedule_name,
        tax_year=context.tax_year,
        evidence=evidence_name,
        boxes=boxes_text,
        alternatives=alternatives_text,
    )

    why_text = default_template["why"].format(
        why=gap.reason,
        guidance_doc="policy guidance",
    )

    # Build upload options
    options = []
    if gap.acceptable_alternatives:
        for alt in gap.acceptable_alternatives:
            options.append(
                UploadOption(
                    label=f"Upload {alt} (PDF/CSV)",
                    accepted_formats=["pdf", "csv"],
                    upload_endpoint=f"/v1/ingest/upload?tag={alt}",
                )
            )
    else:
        options.append(
            UploadOption(
                label=f"Upload {evidence_name} (PDF/CSV)",
                accepted_formats=["pdf", "csv"],
                upload_endpoint=f"/v1/ingest/upload?tag={evidence_name}",
            )
        )

    return ClarifyResponse(
        question_text=question_text,
        why_it_is_needed=why_text,
        citations=gap.citations,
        options_to_provide=options,
        blocking=(gap.role.value == "REQUIRED"),
        boxes_affected=gap.boxes,
    )


async def _validate_boxes_in_kg(policy_dict: dict[str, Any]) -> list[str]:
    """Validate that all referenced boxes exist in KG"""

    if not neo4j_client:
        return ["KG client not available for box validation"]

    errors = []
    all_boxes = set()

    # Collect all box references
    for schedule in policy_dict.get("schedules", {}).values():
        for evidence in schedule.get("evidence", []):
            all_boxes.update(evidence.get("boxes", []))

    if all_boxes:
        try:
            from libs.neo import kg_boxes_exist

            box_existence = await kg_boxes_exist(neo4j_client, list(all_boxes))

            for box_id, exists in box_existence.items():
                if not exists:
                    errors.append(f"Form box '{box_id}' not found in knowledge graph")

        except Exception as e:
            errors.append(f"Failed to validate boxes in KG: {str(e)}")

    return errors


# Health check endpoints
@app.get("/healthz")
async def health_check() -> dict[str, str]:
    """Health check endpoint"""
    return {"status": "healthy", "service": "svc-coverage"}


@app.get("/readyz")
async def readiness_check() -> dict[str, str]:
    """Readiness check endpoint"""
    return {"status": "ready", "service": "svc-coverage"}


@app.get("/livez")
async def liveness_check() -> dict[str, str]:
    """Liveness check endpoint"""
    return {"status": "alive", "service": "svc-coverage"}


# Metrics endpoint (internal only)
@app.get("/metrics")
async def get_metrics_endpoint() -> str:
    """Prometheus metrics endpoint"""
    # This would return Prometheus format metrics
    return "# Coverage service metrics\n"


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, host="0.0.0.0", port=8000)
