"""
Compliance service — Wazuh SCA → compliance_results / compliance_checks.

Two refresh strategies:

- ``refresh_asset_compliance(db, asset_id)``  — one asset, all policies
- ``refresh_all_compliance(db)``               — every Wazuh-linked asset

The per-asset call upserts one row per (asset, policy) and overwrites
the score + counts in place. Old policies that disappear from Wazuh
(e.g. CIS benchmark removed) stay in the table; admins can delete via
the API if they care.

ComplianceCheck rows are NOT populated by these refreshes — that's an
on-demand operation via ``fetch_policy_checks(db, asset_id, policy_id)``
which the deep-dive endpoint calls.
"""
from __future__ import annotations

import json
import logging
from datetime import datetime
from typing import Any, Dict, List, Optional

from sqlalchemy.orm import Session

from app.integrations.wazuh_client import WazuhClient, WazuhAPIError
from app.models.asset import Asset
from app.models.compliance import ComplianceResult, ComplianceCheck

logger = logging.getLogger(__name__)


def _load_wazuh_client(db: Session) -> Optional[WazuhClient]:
    """Build a WazuhClient from the persisted wazuh_config setting.
    Returns None when no config is set — caller treats that as no-op."""
    from app.auth.setting_crypto import read_setting_value
    raw = read_setting_value(db, "wazuh_config")
    if not raw:
        return None
    try:
        cfg = json.loads(raw)
    except (json.JSONDecodeError, TypeError):
        logger.warning("compliance: wazuh_config is not valid JSON")
        return None
    return WazuhClient(
        base_url=cfg.get("api_url"),
        username=cfg.get("username"),
        password=cfg.get("password"),
        indexer_url=cfg.get("indexer_url"),
        indexer_username=cfg.get("indexer_username"),
        indexer_password=cfg.get("indexer_password"),
        verify_ssl=cfg.get("verify_ssl", False),
    )


def _parse_end_scan(raw: Any) -> Optional[datetime]:
    """Wazuh end_scan is an ISO 8601 string or epoch — defensive parse."""
    if not raw:
        return None
    if isinstance(raw, (int, float)):
        try:
            return datetime.utcfromtimestamp(raw)
        except (OverflowError, OSError, ValueError):
            return None
    if isinstance(raw, str):
        # Wazuh sometimes ships with trailing 'Z' or microseconds — try a
        # couple of formats before giving up.
        for fmt in (
            "%Y-%m-%dT%H:%M:%SZ",
            "%Y-%m-%dT%H:%M:%S.%fZ",
            "%Y-%m-%dT%H:%M:%S",
        ):
            try:
                return datetime.strptime(raw, fmt)
            except ValueError:
                continue
    return None


def _upsert_result(
    db: Session,
    asset_id: int,
    policy: Dict[str, Any],
) -> ComplianceResult:
    """Insert or overwrite the per-policy summary row."""
    policy_id = str(policy.get("policy_id") or policy.get("id") or "").strip()
    if not policy_id:
        raise ValueError("policy entry missing policy_id")

    row = (
        db.query(ComplianceResult)
        .filter(
            ComplianceResult.asset_id == asset_id,
            ComplianceResult.policy_id == policy_id,
        )
        .first()
    )
    if row is None:
        row = ComplianceResult(asset_id=asset_id, policy_id=policy_id)
        db.add(row)

    row.policy_name = (policy.get("name") or policy.get("policy") or policy_id)[:255]
    row.policy_description = policy.get("description")
    # Wazuh keys are slightly inconsistent across versions — try both spellings.
    row.pass_count = int(policy.get("pass") or policy.get("passed") or 0)
    row.fail_count = int(policy.get("fail") or policy.get("failed") or 0)
    row.not_applicable_count = int(
        policy.get("invalid") or policy.get("not_applicable") or 0
    )
    row.total_checks = int(
        policy.get("total_checks")
        or (row.pass_count + row.fail_count + row.not_applicable_count)
    )
    row.end_scan = _parse_end_scan(policy.get("end_scan"))
    row.last_synced = datetime.utcnow()
    row.recalc_score()
    return row


def refresh_asset_compliance(
    db: Session,
    asset_id: int,
    client: Optional[WazuhClient] = None,
) -> Dict[str, Any]:
    """
    Refresh all SCA policy results for one asset.

    Returns a stats dict: {asset_id, policies_synced, errors[]}.
    """
    asset = db.query(Asset).filter(Asset.id == asset_id).first()
    if not asset:
        raise ValueError(f"asset {asset_id} not found")
    if not asset.wazuh_agent_id:
        # Nessus-only / manual asset — SCA not available.
        return {
            "asset_id": asset_id,
            "policies_synced": 0,
            "errors": ["asset has no wazuh_agent_id; SCA only works via Wazuh"],
        }

    owned_client = False
    if client is None:
        client = _load_wazuh_client(db)
        owned_client = True
        if client is None:
            return {
                "asset_id": asset_id,
                "policies_synced": 0,
                "errors": ["wazuh_config not set"],
            }

    stats: Dict[str, Any] = {"asset_id": asset_id, "policies_synced": 0, "errors": []}
    try:
        policies = client.get_sca_policies(asset.wazuh_agent_id)
        for p in policies:
            try:
                _upsert_result(db, asset_id, p)
                stats["policies_synced"] += 1
            except Exception as e:
                stats["errors"].append(f"policy {p.get('policy_id')}: {e}")
                logger.warning(
                    "compliance: upsert failed for asset=%s policy=%s: %s",
                    asset_id, p.get("policy_id"), e,
                )
        db.commit()
    except WazuhAPIError as e:
        stats["errors"].append(f"wazuh API: {e}")
        db.rollback()
    except Exception as e:
        logger.exception("compliance refresh failed for asset=%s", asset_id)
        stats["errors"].append(str(e))
        db.rollback()
    finally:
        if owned_client:
            try:
                client.close()
            except Exception:
                pass
    return stats


def refresh_all_compliance(db: Session) -> Dict[str, Any]:
    """Loop over every Wazuh-linked asset and refresh its SCA results."""
    client = _load_wazuh_client(db)
    if client is None:
        return {
            "assets_synced": 0,
            "policies_synced": 0,
            "errors": ["wazuh_config not set"],
        }

    # No status filter — Wazuh keeps SCA results for disconnected agents
    # in the indexer too, so we let the API decide what's available.
    # An asset that returns 0 policies is logged but not treated as
    # an error.
    assets = db.query(Asset).filter(Asset.wazuh_agent_id.isnot(None)).all()
    overall = {
        "assets_synced": 0,
        "policies_synced": 0,
        "assets_no_data": 0,
        "errors": [],
    }
    try:
        for asset in assets:
            r = refresh_asset_compliance(db, asset.id, client=client)
            overall["assets_synced"] += 1
            overall["policies_synced"] += r["policies_synced"]
            overall["errors"].extend(r["errors"])
            if r["policies_synced"] == 0 and not r["errors"]:
                overall["assets_no_data"] += 1
                logger.info(
                    "compliance: asset %s (%s) returned 0 SCA policies — "
                    "agent likely disconnected or SCA module not enabled",
                    asset.id, asset.hostname,
                )
    finally:
        try:
            client.close()
        except Exception:
            pass
    logger.info(
        "compliance: refreshed %d assets, %d policy results, %d errors",
        overall["assets_synced"], overall["policies_synced"], len(overall["errors"]),
    )
    return overall


def fetch_policy_checks(
    db: Session,
    asset_id: int,
    policy_id: str,
) -> List[ComplianceCheck]:
    """
    On-demand fetch + persist of per-check rows for one (asset, policy).

    Drops any previously-cached rows for the policy and rewrites — cheaper
    than a per-check upsert and the check_id is internal to a policy
    version, no good cross-run key.
    """
    asset = db.query(Asset).filter(Asset.id == asset_id).first()
    if not asset or not asset.wazuh_agent_id:
        return []
    result_row = (
        db.query(ComplianceResult)
        .filter(
            ComplianceResult.asset_id == asset_id,
            ComplianceResult.policy_id == policy_id,
        )
        .first()
    )
    if result_row is None:
        return []

    client = _load_wazuh_client(db)
    if client is None:
        return []
    try:
        raw = client.get_sca_checks(asset.wazuh_agent_id, policy_id)
    finally:
        try:
            client.close()
        except Exception:
            pass

    # Wipe + rewrite — small enough table per policy that we don't need
    # finer-grained sync.
    db.query(ComplianceCheck).filter(ComplianceCheck.result_id == result_row.id).delete()
    persisted: List[ComplianceCheck] = []
    for c in raw:
        chk = ComplianceCheck(
            result_id=result_row.id,
            check_id=str(c.get("id") or c.get("check_id") or "")[:64] or "?",
            title=(c.get("title") or "")[:500] or None,
            description=c.get("description"),
            rationale=c.get("rationale"),
            remediation=c.get("remediation"),
            result=(c.get("result") or "?")[:20],
            severity=(c.get("severity") or None) and str(c["severity"])[:20],
        )
        db.add(chk)
        persisted.append(chk)
    db.commit()
    return persisted
