
# compliance/api.py  FastAPI compliance endpoints.

# Registers a /api/compliance router with the following routes:
#     GET  /api/compliance/audit-trail           List all findings with standards mapping
#     GET  /api/compliance/audit-trail/{id}      Full trail + remediation history + sign-offs
#     POST /api/compliance/audit-trail           Create trail from run_id (auto-maps standards)
#     POST /api/compliance/mitigations/{id}      Add mitigation record to a finding
#     POST /api/compliance/sign-off/{id}         Add sign-off to a finding
#     GET  /api/compliance/export/json           Export all trails as JSON
#     GET  /api/compliance/export/pdf            Export compliance report as PDF (via pandoc/weasyprint)
#     GET  /api/compliance/baselines             List stored baseline profiles (Gap #1)
#     GET  /api/compliance/delta-reports/{id}    Get a delta report by ID (Gap #1)
#     GET  /api/compliance/quantization-reports  List quantization variance reports (Gap #2)

# Mount with:
#     from sichgate.compliance.api import build_compliance_router
#     app.include_router(build_compliance_router(db_manager))

#must do for front end phaser ASAP!!!!

from __future__ import annotations

import json
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional

from fastapi import APIRouter, HTTPException
from fastapi.responses import FileResponse, JSONResponse
from pydantic import BaseModel

from sichgate.compliance.mapper import ComplianceMapper
from sichgate.compliance.models import (
    ComplianceAuditTrail,
    Mitigation,
    SignOff,
    _make_finding_id,
)


class CreateAuditTrailRequest(BaseModel):
    run_id: str
    model_target: Optional[str] = None


class MitigationRequest(BaseModel):
    description: str
    mitigation_type: str = "other"
    added_by: str = "compliance_officer"
    retest_scheduled_at: Optional[str] = None


class SignOffRequest(BaseModel):
    approver: str
    role: str = "compliance_officer"
    notes: Optional[str] = None



def build_compliance_router(db_manager=None) -> APIRouter:
    """
    Build and return the /api/compliance FastAPI router.

    Args:
        db_manager: DatabaseManager instance (optional; creates one if not given).

    Returns:
        APIRouter ready for app.include_router(router).
    """
    router = APIRouter(prefix="/api/compliance", tags=["compliance"])
    mapper = ComplianceMapper()

    def _get_db():
        if db_manager is not None:
            return db_manager
        from sichgate.database import DatabaseManager
        return DatabaseManager()

    @router.get("/audit-trail")
    async def list_audit_trails(limit: int = 100) -> List[dict]:
        """Return paginated compliance findings with standards mapping."""
        db = _get_db()
        return db.list_compliance_findings(limit=limit)

  

    @router.get("/audit-trail/{finding_id}")
    async def get_audit_trail(finding_id: str) -> dict:
        """Return full audit trail for a specific finding including mitigations and sign-offs."""
        db = _get_db()
        from sichgate.database import (
            get_session_factory,
            ComplianceAuditTrailRecord,
            MitigationRecord,
            SignOffRecord,
        )
        SessionFactory = get_session_factory()
        with SessionFactory() as session:
            trail = session.query(ComplianceAuditTrailRecord).filter_by(
                finding_id=finding_id
            ).first()
            if not trail:
                raise HTTPException(status_code=404, detail=f"Finding '{finding_id}' not found.")

            mitigations = session.query(MitigationRecord).filter_by(finding_id=finding_id).all()
            sign_offs = session.query(SignOffRecord).filter_by(finding_id=finding_id).all()

            return {
                "finding_id": trail.finding_id,
                "attack_type": trail.attack_type,
                "attack_name": trail.attack_name,
                "vulnerability_type": trail.vulnerability_type,
                "status": trail.status,
                "payload": trail.payload,
                "standards": json.loads(trail.standards_json or "{}"),
                "created_at": trail.created_at.isoformat() if trail.created_at else None,
                "last_tested_at": trail.last_tested_at.isoformat() if trail.last_tested_at else None,
                "mitigations": [
                    {
                        "id": m.id,
                        "added_by": m.added_by,
                        "description": m.description,
                        "mitigation_type": m.mitigation_type,
                        "applied_at": m.applied_at.isoformat() if m.applied_at else None,
                        "retest_scheduled_at": m.retest_scheduled_at.isoformat() if m.retest_scheduled_at else None,
                    }
                    for m in mitigations
                ],
                "sign_offs": [
                    {
                        "id": s.id,
                        "approver": s.approver,
                        "role": s.role,
                        "approved_at": s.approved_at.isoformat() if s.approved_at else None,
                        "notes": s.notes,
                    }
                    for s in sign_offs
                ],
            }


    @router.post("/audit-trail")
    async def create_audit_trails(request: CreateAuditTrailRequest) -> dict:
        """
        Generate audit trail entries for all failing/warning findings in a run.
        Standards mapping is applied automatically.
        """
        db = _get_db()
        run = db.get_run(request.run_id)
        if not run:
            raise HTTPException(status_code=404, detail=f"Run '{request.run_id}' not found.")

        findings = mapper.map_run(run)

        from sichgate.database import (
            get_session_factory, ComplianceAuditTrailRecord
        )
        SessionFactory = get_session_factory()
        created = []

        with SessionFactory() as session:
            for item in findings:
                trail_data = item["trail"]
                standards = item["standards"]

                # Idempotent: skip if finding_id already exists
                existing = session.query(ComplianceAuditTrailRecord).filter_by(
                    finding_id=trail_data["finding_id"]
                ).first()
                if existing:
                    created.append({"finding_id": trail_data["finding_id"], "created": False})
                    continue

                rec = ComplianceAuditTrailRecord(
                    id=trail_data["id"],
                    finding_id=trail_data["finding_id"],
                    run_id=trail_data["run_id"],
                    attack_type=trail_data["attack_type"],
                    attack_name=trail_data["attack_name"],
                    seed=trail_data.get("seed"),
                    payload=trail_data.get("payload"),
                    vulnerability_type=trail_data["vulnerability_type"],
                    status="NEW",
                    created_at=datetime.utcnow(),
                    standards_json=json.dumps(standards),
                )
                session.add(rec)
                created.append({"finding_id": trail_data["finding_id"], "created": True})
            session.commit()

        return {
            "run_id": request.run_id,
            "findings_created": sum(1 for c in created if c["created"]),
            "findings_existing": sum(1 for c in created if not c["created"]),
            "findings": created,
        }


    @router.post("/mitigations/{finding_id}")
    async def add_mitigation(finding_id: str, request: MitigationRequest) -> dict:
        """Add a mitigation record to a finding and update its status."""
        from sichgate.database import (
            get_session_factory, ComplianceAuditTrailRecord, MitigationRecord
        )
        SessionFactory = get_session_factory()
        with SessionFactory() as session:
            trail = session.query(ComplianceAuditTrailRecord).filter_by(
                finding_id=finding_id
            ).first()
            if not trail:
                raise HTTPException(status_code=404, detail=f"Finding '{finding_id}' not found.")

            retest_dt = None
            if request.retest_scheduled_at:
                try:
                    retest_dt = datetime.fromisoformat(request.retest_scheduled_at)
                except ValueError:
                    raise HTTPException(
                        status_code=422,
                        detail="retest_scheduled_at must be ISO 8601 format (e.g. 2026-03-01T09:00:00Z)"
                    )

            mitigation = MitigationRecord(
                id=str(uuid.uuid4()),
                finding_id=finding_id,
                added_by=request.added_by,
                description=request.description,
                mitigation_type=request.mitigation_type,
                applied_at=datetime.utcnow(),
                retest_scheduled_at=retest_dt,
            )
            session.add(mitigation)

            # Advance status
            if trail.status == "NEW":
                trail.status = "MITIGATED"
            session.commit()

            return {
                "finding_id": finding_id,
                "mitigation_id": mitigation.id,
                "status": trail.status,
                "retest_scheduled_at": request.retest_scheduled_at,
            }



    @router.post("/sign-off/{finding_id}")
    async def add_sign_off(finding_id: str, request: SignOffRequest) -> dict:
        """Record a compliance officer / CISO sign-off on a finding."""
        from sichgate.database import (
            get_session_factory, ComplianceAuditTrailRecord, SignOffRecord
        )
        SessionFactory = get_session_factory()
        with SessionFactory() as session:
            trail = session.query(ComplianceAuditTrailRecord).filter_by(
                finding_id=finding_id
            ).first()
            if not trail:
                raise HTTPException(status_code=404, detail=f"Finding '{finding_id}' not found.")

            sign_off = SignOffRecord(
                id=str(uuid.uuid4()),
                finding_id=finding_id,
                approver=request.approver,
                role=request.role,
                approved_at=datetime.utcnow(),
                notes=request.notes,
            )
            session.add(sign_off)

            # Auto-resolve if signed off by privileged role
            PRIVILEGED = {"compliance_officer", "ciso", "legal"}
            if request.role in PRIVILEGED and trail.status == "MITIGATED":
                trail.status = "RESOLVED"

            session.commit()
            return {
                "finding_id": finding_id,
                "sign_off_id": sign_off.id,
                "approver": request.approver,
                "role": request.role,
                "approved_at": sign_off.approved_at.isoformat(),
                "finding_status": trail.status,
            }

    

    @router.get("/export/json")
    async def export_json(limit: int = 500) -> dict:
        """Export all compliance findings as a structured JSON report."""
        db = _get_db()
        findings = db.list_compliance_findings(limit=limit)
        return {
            "report_type": "compliance_audit_trail",
            "generated_at": datetime.now(timezone.utc).isoformat(),
            "generated_by": "SichGate Pro",
            "total_findings": len(findings),
            "findings": findings,
        }




    @router.get("/export/pdf")
    async def export_pdf(limit: int = 500) -> Any:
        """
        Export a PDF compliance report.

        Requires either:
          - weasyprint (pip install weasyprint), or
          - pandoc available on PATH.

        Falls back to a JSON download if neither is available.
        """
        db = _get_db()
        findings = db.list_compliance_findings(limit=limit)
        report = {
            "generated_at": datetime.now(timezone.utc).isoformat(),
            "total_findings": len(findings),
            "findings": findings,
        }

        tmp_dir = Path.home() / ".sichgate" / "exports"
        tmp_dir.mkdir(parents=True, exist_ok=True)

        pdf_path = _render_pdf(report, tmp_dir)

        if pdf_path and pdf_path.exists():
            return FileResponse(
                path=str(pdf_path),
                media_type="application/pdf",
                filename="sichgate_compliance_report.pdf",
            )

        # Fallback: return JSON
        json_path = tmp_dir / "compliance_report.json"
        json_path.write_text(json.dumps(report, indent=2))
        return FileResponse(
            path=str(json_path),
            media_type="application/json",
            filename="sichgate_compliance_report.json",
        )



    @router.get("/baselines")
    async def list_baselines(limit: int = 50) -> List[dict]:
        """List stored baseline vulnerability profiles."""
        db = _get_db()
        return db.list_baseline_profiles(limit=limit)



    @router.get("/delta-reports/{report_id}")
    async def get_delta_report(report_id: str) -> dict:
        """Return a comparative vulnerability delta report by ID."""
        db = _get_db()
        report = db.get_delta_report(report_id)
        if not report:
            raise HTTPException(status_code=404, detail=f"Delta report '{report_id}' not found.")
        return report



    @router.get("/standards/{vulnerability_type}")
    async def get_standards(vulnerability_type: str) -> dict:
        """Return full standards reference for a vulnerability type."""
        supported = mapper.list_supported_vulnerability_types()
        if vulnerability_type not in supported:
            raise HTTPException(
                status_code=404,
                detail=f"Unknown vulnerability type '{vulnerability_type}'. "
                       f"Supported: {supported}"
            )
        return mapper.get_standards_reference(vulnerability_type)

    return router




def _render_pdf(report: dict, output_dir: Path) -> Optional[Path]:
   
    # Attempt to render a PDF compliance report.

    # Tries weasyprint first, then pandoc, then gives up.
    # Returns the PDF path or None if rendering failed.
 
    # Build an HTML report
    html = _build_html_report(report)
    html_path = output_dir / "compliance_report.html"
    html_path.write_text(html, encoding="utf-8")

    # Try weasyprint
    try:
        from weasyprint import HTML as WeasyHTML  # type: ignore
        pdf_path = output_dir / "compliance_report.pdf"
        WeasyHTML(string=html).write_pdf(str(pdf_path))
        return pdf_path
    except ImportError:
        pass
    except Exception:
        pass

    # Try pandoc
    try:
        import subprocess
        pdf_path = output_dir / "compliance_report.pdf"
        result = subprocess.run(
            ["pandoc", str(html_path), "-o", str(pdf_path), "--pdf-engine=wkhtmltopdf"],
            capture_output=True,
            timeout=30,
        )
        if result.returncode == 0 and pdf_path.exists():
            return pdf_path
    except (FileNotFoundError, subprocess.TimeoutExpired):
        pass

    return None


def _build_html_report(report: dict) -> str:
    """Build a minimal HTML compliance report from findings data."""
    findings = report.get("findings", [])
    rows = ""
    for f in findings:
        standards_str = json.dumps(f.get("standards", {}), indent=2)
        rows += f"""
        <tr>
            <td>{f.get("finding_id", "")}</td>
            <td>{f.get("attack_name", "")}</td>
            <td>{f.get("vulnerability_type", "")}</td>
            <td><span class="status-{f.get('status', 'NEW').lower()}">{f.get("status", "NEW")}</span></td>
            <td>{f.get("created_at", "")}</td>
            <td><pre>{standards_str}</pre></td>
        </tr>
        """

    return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SichGate Pro — Compliance Audit Report</title>
<style>
  body {{ font-family: Arial, sans-serif; font-size: 12px; margin: 30px; }}
  h1 {{ color: #1a1a2e; }}
  h2 {{ color: #16213e; border-bottom: 2px solid #e94560; padding-bottom: 6px; }}
  table {{ width: 100%; border-collapse: collapse; margin-top: 16px; }}
  th {{ background: #1a1a2e; color: white; padding: 8px; text-align: left; }}
  td {{ padding: 8px; border-bottom: 1px solid #ddd; vertical-align: top; }}
  tr:nth-child(even) {{ background: #f9f9f9; }}
  pre {{ font-size: 10px; white-space: pre-wrap; margin: 0; }}
  .status-new {{ color: #e94560; font-weight: bold; }}
  .status-mitigated {{ color: #f5a623; font-weight: bold; }}
  .status-resolved {{ color: #27ae60; font-weight: bold; }}
  .status-accepted_risk {{ color: #95a5a6; font-weight: bold; }}
  .meta {{ color: #666; font-size: 11px; margin-bottom: 20px; }}
</style>
</head>
<body>
<h1>SichGate Pro — Compliance Audit Report</h1>
<p class="meta">
  Generated: {report.get("generated_at", "")}<br>
  Total Findings: {report.get("total_findings", 0)}<br>
  Generated by: SichGate Pro (on-premises)
</p>

<h2>Compliance Findings</h2>
<table>
  <thead>
    <tr>
      <th>Finding ID</th>
      <th>Attack</th>
      <th>Vulnerability Type</th>
      <th>Status</th>
      <th>Discovered</th>
      <th>Standards Mapping</th>
    </tr>
  </thead>
  <tbody>
    {rows if rows else "<tr><td colspan='6'>No findings to display.</td></tr>"}
  </tbody>
</table>

<p style="margin-top: 40px; font-size: 10px; color: #999;">
  This report was generated by SichGate Pro. All testing conducted on-premises.
  No model data was transmitted externally.
</p>
</body>
</html>"""
