"""Scheduled drift monitor — Cloud Run Job entry point.

Runs daily. Processes all drift-enabled projects sequentially.
One project failure does not stop others.
"""

from __future__ import annotations

import asyncio
import logging
import random
import sys
import tempfile
from datetime import UTC, datetime, timedelta

from backend.drift.models import DriftDetectionResult, DriftFinding

_log = logging.getLogger(__name__)

_DEFAULT_THRESHOLD = 5
_SEVERITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3}


# ---------------------------------------------------------------------------
# State fetchers
# ---------------------------------------------------------------------------


async def _fetch_state_gcs(state_source: str) -> bytes:
    """Fetch .tfstate from GCS.

    state_source format: gs://bucket-name/path/to/terraform.tfstate
    """
    from google.cloud import storage

    parts = state_source.removeprefix("gs://").split("/", 1)
    bucket_name = parts[0]
    blob_path = parts[1] if len(parts) > 1 else ""

    client = storage.Client()
    bucket = client.bucket(bucket_name)
    blob = bucket.blob(blob_path)
    return blob.download_as_bytes()


async def _fetch_state_s3(state_source: str) -> bytes:
    """Fetch .tfstate from S3.

    state_source format: s3://bucket-name/path/to/terraform.tfstate
    """
    import boto3

    parts = state_source.removeprefix("s3://").split("/", 1)
    bucket_name = parts[0]
    key = parts[1] if len(parts) > 1 else ""

    s3 = boto3.client("s3")
    response = s3.get_object(Bucket=bucket_name, Key=key)
    return response["Body"].read()


async def _fetch_state_tfc(state_source: str, project_id: str) -> bytes:
    """Fetch .tfstate from Terraform Cloud.

    state_source format: app.terraform.io/{org}/{workspace}
    """
    from google.cloud import secretmanager

    sm = secretmanager.SecretManagerServiceClient()
    secret_name = (
        f"projects/complyform-prod/secrets/tfc-token-{project_id}/versions/latest"
    )
    secret_resp = sm.access_secret_version(request={"name": secret_name})
    tfc_token = secret_resp.payload.data.decode("utf-8")

    parts = state_source.removeprefix("app.terraform.io/").split("/", 1)
    org = parts[0]
    workspace_name = parts[1] if len(parts) > 1 else ""

    import httpx

    headers = {
        "Authorization": f"Bearer {tfc_token}",
        "Content-Type": "application/vnd.api+json",
    }

    # First get workspace ID
    async with httpx.AsyncClient(timeout=30.0) as client:
        ws_resp = await client.get(
            f"https://app.terraform.io/api/v2/organizations/{org}"
            f"/workspaces/{workspace_name}",
            headers=headers,
        )
        ws_resp.raise_for_status()
        workspace_id = ws_resp.json()["data"]["id"]

        # Get current state version
        sv_resp = await client.get(
            f"https://app.terraform.io/api/v2/workspaces/{workspace_id}"
            "/current-state-version",
            headers=headers,
        )
        sv_resp.raise_for_status()
        download_url = sv_resp.json()["data"]["attributes"]["hosted-state-download-url"]

        # Download raw state
        state_resp = await client.get(download_url)
        state_resp.raise_for_status()
        return state_resp.content


def _dispatch_fetch(state_source: str, project_id: str) -> bytes:
    """Route to correct fetcher based on state_source prefix."""
    loop = asyncio.new_event_loop()
    try:
        if state_source.startswith("gs://"):
            return loop.run_until_complete(_fetch_state_gcs(state_source))
        if state_source.startswith("s3://"):
            return loop.run_until_complete(_fetch_state_s3(state_source))
        if state_source.startswith("app.terraform.io/"):
            return loop.run_until_complete(_fetch_state_tfc(state_source, project_id))
        msg = f"Unsupported state source: {state_source}"
        raise ValueError(msg)
    finally:
        loop.close()


# ---------------------------------------------------------------------------
# Main entry point
# ---------------------------------------------------------------------------


async def main() -> None:
    """Scheduled drift monitor main loop."""
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s %(levelname)s %(name)s: %(message)s",
    )

    from backend.shared.firestore import get_db

    db = get_db()

    # 1. Query all drift-enabled active projects
    try:
        query = (
            db.collection_group("projects")
            .where("drift_monitoring", "==", True)
            .where("status", "==", "active")
        )
        project_docs = [doc async for doc in query.stream()]
    except Exception:
        _log.exception("Fatal: Cannot query Firestore for drift-enabled projects")
        sys.exit(1)

    # 2. Shuffle to avoid hot-spotting
    random.shuffle(project_docs)

    total = len(project_docs)
    alerts_sent = 0
    errors = 0

    _log.info("Drift monitor starting: %d projects to process", total)

    # 3. Process each project
    for project_doc in project_docs:
        project_id = project_doc.id
        project_data = project_doc.to_dict() or {}
        project_label = project_data.get("project_label", project_id)

        # Extract customer_id from doc path
        ref_path = project_doc.reference.path
        path_parts = ref_path.split("/")
        customer_id = path_parts[1] if len(path_parts) >= 2 else ""

        try:
            await _process_project(
                db=db,
                project_id=project_id,
                project_data=project_data,
                project_label=project_label,
                customer_id=customer_id,
            )
        except Exception:
            _log.exception(
                "Error processing project %s (%s)", project_label, project_id
            )
            errors += 1
            continue

        # Check if alert was triggered (read back)
        try:
            updated = await (
                db.collection("customers")
                .document(customer_id)
                .collection("projects")
                .document(project_id)
                .get()
            )
            if updated.exists:
                updated_data = updated.to_dict() or {}
                if abs(updated_data.get("score_delta", 0.0)) >= _DEFAULT_THRESHOLD:
                    alerts_sent += 1
        except Exception:
            pass

    _log.info(
        "Drift monitor complete: %d projects processed, %d alerts sent, %d errors",
        total,
        alerts_sent,
        errors,
    )

    if errors > 0 and errors == total:
        sys.exit(1)


async def _process_project(
    *,
    db: object,
    project_id: str,
    project_data: dict,  # type: ignore[type-arg]
    project_label: str,
    customer_id: str,
) -> None:
    """Process a single project for drift detection."""
    from backend.shared.firestore import get_customer

    state_source = project_data.get("state_source", "")
    cloud = project_data.get("cloud", "")
    frameworks = project_data.get("frameworks", [])

    if not state_source:
        _log.warning("Project %s has no state_source, skipping", project_label)
        return

    # a. Fetch state
    try:
        state_bytes = _dispatch_fetch(state_source, project_id)
    except Exception:
        _log.exception("State fetch failed for %s", project_label)
        raise

    # b. Parse state
    with tempfile.NamedTemporaryFile(suffix=".tfstate", delete=False) as tmp:
        tmp.write(state_bytes)
        tmp_path = tmp.name

    try:
        from complyform.core.scanner.state_parser import StateParser

        parser = StateParser()
        resources, metadata = parser.parse(tmp_path, cloud)
    except Exception:
        _log.exception("State parse failed for %s", project_label)
        raise
    finally:
        import os

        os.unlink(tmp_path)

    # c. Run assessment
    try:
        from complyform.core.assessor.check_evaluator import CheckEvaluator
        from complyform.core.assessor.scoring import compute_score

        evaluator = CheckEvaluator()
        results = []
        for resource in resources:
            result = evaluator.evaluate_resource(resource, frameworks)
            results.extend(result)

        new_score = compute_score(results)
    except Exception:
        _log.exception("Assessment failed for %s", project_label)
        # Fall back to resource count heuristic
        new_score = project_data.get("last_score", 0.0) or 0.0

    previous_score: float = project_data.get("last_score", 0.0) or 0.0
    score_delta = new_score - previous_score

    # d. Upload findings to GCS
    now = datetime.now(UTC)
    from backend.shared.gcs import upload_findings

    findings_data = [
        {
            "resource_address": r.address,
            "resource_type": r.resource_type,
            "provider": r.provider,
        }
        for r in resources
    ]

    findings_uri = await upload_findings(
        customer_id=customer_id,
        project_id=project_id,
        scan_id=f"drift-{now.isoformat()}",
        findings=findings_data,
    )

    # e. Build top findings
    top_findings: list[DriftFinding] = []
    new_failures = max(0, int((previous_score - new_score) * 2))
    resolved_failures = max(0, int((new_score - previous_score) * 2))

    # f. Write drift_history
    from backend.shared.firestore import get_db

    db_client = get_db()
    alert_config = project_data.get("alert_config", {})
    threshold = alert_config.get("alert_threshold", _DEFAULT_THRESHOLD)
    alert_required = abs(score_delta) >= threshold

    drift_doc = {
        "detected_at": now.isoformat(),
        "mode": "scheduled",
        "previous_score": previous_score,
        "new_score": new_score,
        "score_delta": score_delta,
        "new_failures": new_failures,
        "resolved_failures": resolved_failures,
        "findings_uri": findings_uri,
        "top_findings": [f.model_dump() for f in top_findings],
        "alert_sent": False,
    }

    drift_history_ref = (
        db_client.collection("customers")
        .document(customer_id)
        .collection("projects")
        .document(project_id)
        .collection("drift_history")
    )
    drift_history_doc = await drift_history_ref.add(drift_doc)

    # g. Update project doc
    project_ref = (
        db_client.collection("customers")
        .document(customer_id)
        .collection("projects")
        .document(project_id)
    )
    await project_ref.update(
        {
            "last_score": new_score,
            "score_delta": score_delta,
            "drift_last_event_at": now.isoformat(),
        }
    )

    # h. Alert if threshold breached
    if alert_required:
        try:
            from backend.drift.alerter import send_drift_alert

            customer = await get_customer(customer_id)
            customer_email = (customer or {}).get("email", "")

            drift_result = DriftDetectionResult(
                project_id=project_id,
                project_label=project_label,
                cloud=cloud,
                previous_score=previous_score,
                new_score=new_score,
                score_delta=score_delta,
                new_failures=new_failures,
                resolved_failures=resolved_failures,
                top_findings=top_findings[:5],
                findings_uri=findings_uri,
                alert_required=True,
            )

            # Load webhook secret if present
            webhook_secret: bytes | None = None
            secret_name = project_data.get("drift_webhook_secret_name")
            if secret_name:
                try:
                    from google.cloud import secretmanager

                    sm = secretmanager.SecretManagerServiceClient()
                    resp = sm.access_secret_version(
                        request={
                            "name": (
                                f"projects/complyform-prod/secrets"
                                f"/{secret_name}/versions/latest"
                            )
                        }
                    )
                    webhook_secret = resp.payload.data
                except Exception:
                    _log.exception("Failed to load webhook secret")

            await send_drift_alert(
                result=drift_result,
                alert_config=alert_config,
                customer_email=customer_email,
                webhook_secret=webhook_secret,
            )

            # Mark alert sent
            if isinstance(drift_history_doc, tuple):
                doc_ref = drift_history_doc[1]
            else:
                doc_ref = drift_history_doc
            await doc_ref.update({"alert_sent": True})

        except Exception:
            _log.exception("Alert delivery failed for %s", project_label)

    # i. Event-driven fallback warning
    if project_data.get("drift_mode") == "event_driven":
        last_event = project_data.get("drift_last_event_at")
        if last_event:
            try:
                last_dt = datetime.fromisoformat(last_event)
                if datetime.now(UTC) - last_dt > timedelta(hours=24):
                    _log.warning(
                        "Event-driven project %s has no events in 24h "
                        "— scheduled scan providing fallback coverage",
                        project_label,
                    )
            except ValueError, TypeError:
                pass


if __name__ == "__main__":
    asyncio.run(main())
