"""Compliance presets for regulated industries.

Three named presets bundle enterprise safety features into a single flag:

- **DEVELOPMENT** — audit logging + WAL + AI content labels.
- **STANDARD** — adds HMAC audit chain, governed workflow, approval gates,
  execution fingerprint.
- **REGULATED** — adds signed WAL, data residency, SBOM generation,
  mandatory human review, evidence bundle export.

Each sub-feature can also be enabled independently outside of presets.

Usage::

    config = ComplianceConfig.from_preset(CompliancePreset.STANDARD)
    # Override individual features:
    config = dataclasses.replace(config, sbom_enabled=True)
"""

from __future__ import annotations

import dataclasses
import hashlib
import json
import logging
import re
import time
from dataclasses import dataclass
from enum import Enum
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from pathlib import Path

logger = logging.getLogger(__name__)


class CompliancePreset(Enum):
    """Named compliance tiers with escalating requirements."""

    DEVELOPMENT = "development"
    STANDARD = "standard"
    REGULATED = "regulated"


@dataclass
class ComplianceConfig:
    """Full compliance configuration derived from a preset or individual toggles.

    Attributes:
        preset: The named preset that produced this config (None if ad-hoc).
        audit_logging: Append-only JSONL audit log.
        audit_hmac_chain: HMAC-chained tamper-evident audit log.
        wal_enabled: Write-ahead log for crash-safe decisions.
        wal_signed: Ed25519/HMAC-signed WAL entries.
        ai_content_labels: Prepend AI-generated header to modified files.
        ai_label_format: Format string for the AI content label header.
        governed_workflow: Deterministic phase-based execution.
        approval_gates: Human sign-off at review phase.
        mandatory_human_review: Human review required for ALL tasks.
        execution_fingerprint: Compute and store determinism fingerprint.
        data_residency: Restrict model endpoints to a specific region.
        data_residency_region: Region identifier (e.g. "eu").
        sbom_enabled: Generate CycloneDX SBOM after each task.
        evidence_bundle: Auto-export execution evidence on run completion.
    """

    preset: CompliancePreset | None = None
    audit_logging: bool = False
    audit_hmac_chain: bool = False
    wal_enabled: bool = False
    wal_signed: bool = False
    ai_content_labels: bool = False
    ai_label_format: str = "// Generated by AI agent (Bernstein)"
    governed_workflow: bool = False
    approval_gates: bool = False
    mandatory_human_review: bool = False
    execution_fingerprint: bool = False
    data_residency: bool = False
    data_residency_region: str = ""
    sbom_enabled: bool = False
    evidence_bundle: bool = False

    @classmethod
    def from_preset(cls, preset: CompliancePreset) -> ComplianceConfig:
        """Build a full configuration from a named preset.

        Args:
            preset: The compliance tier to activate.

        Returns:
            ComplianceConfig with all features set to the preset's defaults.
        """
        if preset == CompliancePreset.DEVELOPMENT:
            return cls(
                preset=preset,
                audit_logging=True,
                wal_enabled=True,
                ai_content_labels=True,
            )

        if preset == CompliancePreset.STANDARD:
            return cls(
                preset=preset,
                audit_logging=True,
                audit_hmac_chain=True,
                wal_enabled=True,
                ai_content_labels=True,
                governed_workflow=True,
                approval_gates=True,
                execution_fingerprint=True,
            )

        # REGULATED
        return cls(
            preset=preset,
            audit_logging=True,
            audit_hmac_chain=True,
            wal_enabled=True,
            wal_signed=True,
            ai_content_labels=True,
            governed_workflow=True,
            approval_gates=True,
            mandatory_human_review=True,
            execution_fingerprint=True,
            data_residency=True,
            data_residency_region="eu",
            sbom_enabled=True,
            evidence_bundle=True,
        )

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> ComplianceConfig:
        """Parse compliance config from a YAML/dict mapping.

        Accepts either ``compliance: standard`` (string -> preset) or a
        mapping with individual feature toggles.

        Args:
            data: Mapping from the ``compliance`` section of bernstein.yaml.

        Returns:
            Fully resolved ComplianceConfig.
        """
        # If the value is just a preset name string, delegate to from_preset.
        if isinstance(data, str):
            return cls.from_preset(CompliancePreset(data.lower()))

        preset_raw = data.get("preset")
        base = cls.from_preset(CompliancePreset(str(preset_raw).lower())) if preset_raw is not None else cls()

        # Override individual fields from the mapping.
        overrides: dict[str, Any] = {}
        field_names = {f.name for f in dataclasses.fields(cls) if f.name != "preset"}
        for key, value in data.items():
            if key == "preset":
                continue
            if key in field_names:
                overrides[key] = value
        if overrides:
            base = dataclasses.replace(base, **overrides)
        return base

    def check_prerequisites(self) -> list[str]:
        """Check for missing prerequisites based on the active features.

        Returns:
            List of human-readable warnings (empty when everything is OK).
        """
        warnings: list[str] = []

        if self.audit_hmac_chain and not self.audit_logging:
            warnings.append("HMAC audit chain requires audit_logging to be enabled")

        if self.wal_signed and not self.wal_enabled:
            warnings.append("Signed WAL requires wal_enabled to be enabled")

        if self.governed_workflow and not self.wal_enabled:
            warnings.append("Governed workflow requires WAL for crash-safe decision history")

        if self.mandatory_human_review and not self.approval_gates:
            warnings.append("Mandatory human review requires approval_gates to be enabled")

        if self.data_residency and not self.data_residency_region:
            warnings.append("Data residency enabled but no region specified (set data_residency_region)")

        if self.evidence_bundle and not self.wal_enabled:
            warnings.append("Evidence bundle export requires WAL to be enabled")

        if self.evidence_bundle and not self.audit_logging:
            warnings.append("Evidence bundle export requires audit_logging to be enabled")

        return warnings

    def to_dict(self) -> dict[str, Any]:
        """Serialize to a JSON-safe dict for persistence and API responses."""
        return {
            "preset": self.preset.value if self.preset else None,
            "audit_logging": self.audit_logging,
            "audit_hmac_chain": self.audit_hmac_chain,
            "wal_enabled": self.wal_enabled,
            "wal_signed": self.wal_signed,
            "ai_content_labels": self.ai_content_labels,
            "ai_label_format": self.ai_label_format,
            "governed_workflow": self.governed_workflow,
            "approval_gates": self.approval_gates,
            "mandatory_human_review": self.mandatory_human_review,
            "execution_fingerprint": self.execution_fingerprint,
            "data_residency": self.data_residency,
            "data_residency_region": self.data_residency_region,
            "sbom_enabled": self.sbom_enabled,
            "evidence_bundle": self.evidence_bundle,
        }


# ---------------------------------------------------------------------------
# AI content labeling
# ---------------------------------------------------------------------------

# Comment prefix by file extension.
_COMMENT_STYLES: dict[str, str] = {
    ".py": "#",
    ".rb": "#",
    ".sh": "#",
    ".bash": "#",
    ".zsh": "#",
    ".yaml": "#",
    ".yml": "#",
    ".toml": "#",
    ".r": "#",
    ".pl": "#",
    ".pm": "#",
    ".js": "//",
    ".ts": "//",
    ".tsx": "//",
    ".jsx": "//",
    ".java": "//",
    ".c": "//",
    ".cpp": "//",
    ".h": "//",
    ".hpp": "//",
    ".cs": "//",
    ".go": "//",
    ".rs": "//",
    ".swift": "//",
    ".kt": "//",
    ".scala": "//",
    ".dart": "//",
    ".css": "/*",
    ".html": "<!--",
    ".xml": "<!--",
    ".sql": "--",
    ".lua": "--",
    ".hs": "--",
}

_COMMENT_CLOSE: dict[str, str] = {
    "/*": " */",
    "<!--": " -->",
}


def ai_label_for_file(filepath: Path, label_text: str) -> str | None:
    """Generate the AI content label line appropriate for a file's language.

    Args:
        filepath: Path to the file being labeled.
        label_text: The label message (e.g. "Generated by AI agent (Bernstein)").

    Returns:
        The formatted comment line, or None if the file type is unknown.
    """
    suffix = filepath.suffix.lower()
    prefix = _COMMENT_STYLES.get(suffix)
    if prefix is None:
        return None
    close = _COMMENT_CLOSE.get(prefix, "")
    return f"{prefix} {label_text}{close}"


# ---------------------------------------------------------------------------
# SBOM generation (CycloneDX stub)
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class SBOMEntry:
    """A single component in the Software Bill of Materials."""

    name: str
    version: str
    purl: str  # Package URL (e.g. "pkg:pypi/requests@2.31.0")


def generate_sbom(
    components: list[SBOMEntry],
    run_id: str,
    output_dir: Path,
) -> Path:
    """Generate a CycloneDX JSON SBOM and write it to disk.

    Args:
        components: List of software components to include.
        run_id: Orchestration run identifier.
        output_dir: Directory to write the SBOM file into.

    Returns:
        Path to the written SBOM file.
    """
    output_dir.mkdir(parents=True, exist_ok=True)
    sbom = {
        "bomFormat": "CycloneDX",
        "specVersion": "1.5",
        "serialNumber": f"urn:uuid:{run_id}",
        "version": 1,
        "metadata": {
            "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
            "tools": [{"vendor": "Bernstein", "name": "bernstein", "version": "1.0.0"}],
        },
        "components": [
            {
                "type": "library",
                "name": entry.name,
                "version": entry.version,
                "purl": entry.purl,
            }
            for entry in components
        ],
    }
    path = output_dir / f"sbom-{run_id}.cdx.json"
    path.write_text(json.dumps(sbom, indent=2))
    logger.info("SBOM written: %s (%d components)", path, len(components))
    return path


# ---------------------------------------------------------------------------
# Evidence bundle export
# ---------------------------------------------------------------------------


def export_evidence_bundle(
    run_id: str,
    sdd_dir: Path,
    output_dir: Path | None = None,
) -> Path:
    """Package compliance evidence into a single directory for export.

    Collects WAL, audit log, workflow events, SBOM, and execution
    fingerprint into ``<output_dir>/evidence-<run_id>/``.

    Args:
        run_id: Orchestration run identifier.
        sdd_dir: Path to the ``.sdd`` directory.
        output_dir: Where to write the bundle. Defaults to ``sdd_dir/evidence/``.

    Returns:
        Path to the evidence bundle directory.
    """
    import shutil

    if output_dir is None:
        output_dir = sdd_dir / "evidence"
    bundle_dir = output_dir / f"evidence-{run_id}"
    bundle_dir.mkdir(parents=True, exist_ok=True)

    # Collect artifacts
    artifacts: list[tuple[str, Path]] = [
        ("wal", sdd_dir / "runtime" / "wal"),
        ("audit", sdd_dir / "audit"),
        ("workflow", sdd_dir / "runtime" / "workflow"),
        ("sbom", sdd_dir / "sbom"),
    ]

    manifest_entries: list[dict[str, str]] = []
    for label, src in artifacts:
        if src.exists():
            dest = bundle_dir / label
            if src.is_dir():
                shutil.copytree(src, dest, dirs_exist_ok=True)
            else:
                shutil.copy2(src, dest)
            manifest_entries.append({"artifact": label, "source": str(src)})

    # Write bundle manifest
    manifest = {
        "run_id": run_id,
        "exported_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
        "artifacts": manifest_entries,
    }
    (bundle_dir / "manifest.json").write_text(json.dumps(manifest, indent=2))
    logger.info("Evidence bundle exported: %s (%d artifacts)", bundle_dir, len(manifest_entries))
    return bundle_dir


# ---------------------------------------------------------------------------
# SOC 2 evidence export
# ---------------------------------------------------------------------------


def parse_period(period: str) -> tuple[str, str]:
    """Parse a period string into ISO 8601 start/end timestamps.

    Supported formats:
      - ``Q1-2026`` through ``Q4-2026`` (quarter)
      - ``2026-03`` (month)
      - ``2026`` (full year)

    Args:
        period: Period string to parse.

    Returns:
        ``(start, end)`` as ISO 8601 date strings (``YYYY-MM-DD``).

    Raises:
        ValueError: If the period string cannot be parsed.
    """
    # Quarter: Q1-2026 .. Q4-2026
    m = re.match(r"^Q([1-4])-(\d{4})$", period, re.IGNORECASE)
    if m:
        quarter = int(m.group(1))
        year = int(m.group(2))
        start_month = (quarter - 1) * 3 + 1
        end_month = quarter * 3
        # Last day of the end month
        if end_month == 12:
            end_day = 31
        elif end_month in (4, 6, 9, 11):
            end_day = 30
        elif end_month == 2:
            # Leap year check
            end_day = 29 if (year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)) else 28
        else:
            end_day = 31
        return f"{year}-{start_month:02d}-01", f"{year}-{end_month:02d}-{end_day:02d}"

    # Month: 2026-03
    m = re.match(r"^(\d{4})-(\d{2})$", period)
    if m:
        year = int(m.group(1))
        month = int(m.group(2))
        if not 1 <= month <= 12:
            msg = f"Invalid month in period: {period!r}"
            raise ValueError(msg)
        # Last day of the month
        if month == 12:
            last_day = 31
        elif month in (4, 6, 9, 11):
            last_day = 30
        elif month == 2:
            last_day = 29 if (year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)) else 28
        else:
            last_day = 31
        return f"{year}-{month:02d}-01", f"{year}-{month:02d}-{last_day}"

    # Year: 2026
    m = re.match(r"^(\d{4})$", period)
    if m:
        year = m.group(1)
        return f"{year}-01-01", f"{year}-12-31"

    msg = f"Cannot parse period: {period!r}. Use Q1-2026, 2026-03, or 2026."
    raise ValueError(msg)


def export_soc2_package(
    sdd_dir: Path,
    period: str,
    output_path: Path | None = None,
    fmt: str = "zip",
) -> Path:
    """Export a SOC 2 evidence package for the given period.

    Collects audit logs, access records, compliance configuration, policy
    versions, HMAC chain verification results, Merkle seals, and SBOM files
    filtered to the requested period.  Produces either a directory or a zip
    archive suitable for delivery to auditors.

    Args:
        sdd_dir: Path to the ``.sdd`` directory.
        period: Period string (e.g. ``Q1-2026``, ``2026-03``, ``2026``).
        output_path: Where to write the package.  Defaults to
            ``sdd_dir / evidence / soc2-<period>``.
        fmt: Output format — ``"zip"`` or ``"dir"``.

    Returns:
        Path to the exported zip file or directory.

    Raises:
        ValueError: If the period cannot be parsed or no audit data exists.
    """
    import shutil

    start_date, end_date = parse_period(period)

    if output_path is None:
        output_path = sdd_dir / "evidence"
    output_path.mkdir(parents=True, exist_ok=True)

    bundle_name = f"soc2-{period}"
    bundle_dir = output_path / bundle_name
    if bundle_dir.exists():
        shutil.rmtree(bundle_dir)
    bundle_dir.mkdir(parents=True)

    artifacts_collected: list[dict[str, Any]] = []

    # --- 1. Audit logs (filtered by period) --------------------------------
    audit_dir = sdd_dir / "audit"
    if audit_dir.is_dir():
        dest = bundle_dir / "audit_logs"
        dest.mkdir()
        for jsonl_file in sorted(audit_dir.glob("*.jsonl")):
            # File names are YYYY-MM-DD.jsonl — filter by date range
            file_date = jsonl_file.stem  # e.g. "2026-01-15"
            if start_date <= file_date <= end_date:
                shutil.copy2(jsonl_file, dest / jsonl_file.name)
        copied = list(dest.iterdir())
        if copied:
            artifacts_collected.append(
                {
                    "type": "audit_logs",
                    "description": "HMAC-chained audit event logs",
                    "file_count": len(copied),
                    "period_filter": f"{start_date} to {end_date}",
                }
            )

    # --- 2. HMAC chain verification ----------------------------------------
    verification: dict[str, Any] = {"hmac_chain": None, "merkle": None}
    if audit_dir.is_dir():
        from bernstein.core.audit import AuditLog

        try:
            audit_log = AuditLog(audit_dir)
            valid, errors = audit_log.verify()
            verification["hmac_chain"] = {
                "valid": valid,
                "errors": errors,
                "verified_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
            }
        except Exception as exc:
            verification["hmac_chain"] = {"valid": False, "errors": [str(exc)]}

    # --- 3. Merkle seals ---------------------------------------------------
    merkle_dir = audit_dir / "merkle" if audit_dir.is_dir() else sdd_dir / "audit" / "merkle"
    if merkle_dir.is_dir():
        dest = bundle_dir / "merkle_seals"
        dest.mkdir()
        for seal_file in sorted(merkle_dir.glob("*.json")):
            shutil.copy2(seal_file, dest / seal_file.name)
        copied = list(dest.iterdir())
        if copied:
            artifacts_collected.append(
                {
                    "type": "merkle_seals",
                    "description": "Merkle tree integrity seals",
                    "file_count": len(copied),
                }
            )

    # --- 4. Compliance configuration ---------------------------------------
    config_dir = sdd_dir / "config"
    if config_dir.is_dir():
        dest = bundle_dir / "compliance_config"
        dest.mkdir()
        for cfg_file in config_dir.iterdir():
            if cfg_file.is_file() and cfg_file.name != "audit-key":
                shutil.copy2(cfg_file, dest / cfg_file.name)
        copied = list(dest.iterdir())
        if copied:
            artifacts_collected.append(
                {
                    "type": "compliance_config",
                    "description": "Compliance and policy configuration",
                    "file_count": len(copied),
                }
            )

    # --- 5. WAL (write-ahead log) ------------------------------------------
    wal_dir = sdd_dir / "runtime" / "wal"
    if wal_dir.is_dir():
        dest = bundle_dir / "wal"
        dest.mkdir()
        for wal_file in sorted(wal_dir.glob("*")):
            if wal_file.is_file():
                shutil.copy2(wal_file, dest / wal_file.name)
        copied = list(dest.iterdir())
        if copied:
            artifacts_collected.append(
                {
                    "type": "wal",
                    "description": "Write-ahead log entries",
                    "file_count": len(copied),
                }
            )

    # --- 6. SBOM -----------------------------------------------------------
    sbom_dir = sdd_dir / "sbom"
    if sbom_dir.is_dir():
        dest = bundle_dir / "sbom"
        dest.mkdir()
        for sbom_file in sorted(sbom_dir.glob("*.json")):
            shutil.copy2(sbom_file, dest / sbom_file.name)
        copied = list(dest.iterdir())
        if copied:
            artifacts_collected.append(
                {
                    "type": "sbom",
                    "description": "Software Bill of Materials (CycloneDX)",
                    "file_count": len(copied),
                }
            )

    # --- 7. Write verification results -------------------------------------
    (bundle_dir / "verification.json").write_text(json.dumps(verification, indent=2))

    # --- 8. Compute package checksum ---------------------------------------
    file_checksums: dict[str, str] = {}
    for path in sorted(bundle_dir.rglob("*")):
        if path.is_file() and path.name != "manifest.json":
            digest = hashlib.sha256(path.read_bytes()).hexdigest()
            file_checksums[str(path.relative_to(bundle_dir))] = digest

    # --- 9. Write manifest -------------------------------------------------
    manifest = {
        "package_type": "soc2-evidence",
        "period": period,
        "period_start": start_date,
        "period_end": end_date,
        "exported_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
        "artifacts": artifacts_collected,
        "verification": verification,
        "file_checksums": file_checksums,
    }
    (bundle_dir / "manifest.json").write_text(json.dumps(manifest, indent=2))

    # --- 10. Optionally zip ------------------------------------------------
    if fmt == "zip":
        zip_path = output_path / f"{bundle_name}.zip"
        shutil.make_archive(str(output_path / bundle_name), "zip", output_path, bundle_name)
        shutil.rmtree(bundle_dir)
        logger.info("SOC 2 evidence package exported: %s", zip_path)
        return zip_path

    logger.info("SOC 2 evidence package exported: %s", bundle_dir)
    return bundle_dir


# ---------------------------------------------------------------------------
# Compliance config persistence
# ---------------------------------------------------------------------------


def persist_compliance_config(config: ComplianceConfig, sdd_dir: Path) -> None:
    """Write the active compliance config to .sdd/config/compliance.json.

    Args:
        config: The compliance configuration to persist.
        sdd_dir: Path to the .sdd directory.
    """
    config_dir = sdd_dir / "config"
    config_dir.mkdir(parents=True, exist_ok=True)
    path = config_dir / "compliance.json"
    path.write_text(json.dumps(config.to_dict(), indent=2))
    logger.info("Compliance config persisted: %s (preset=%s)", path, config.preset)


def load_compliance_config(sdd_dir: Path) -> ComplianceConfig | None:
    """Load compliance config from .sdd/config/compliance.json if it exists.

    Args:
        sdd_dir: Path to the .sdd directory.

    Returns:
        ComplianceConfig if found, None otherwise.
    """
    path = sdd_dir / "config" / "compliance.json"
    if not path.exists():
        return None
    try:
        data = json.loads(path.read_text())
        return ComplianceConfig.from_dict(data)
    except (json.JSONDecodeError, ValueError, KeyError):
        logger.warning("Failed to load compliance config from %s", path)
        return None
