"""
Provenance sidecar utilities.

Responsible for:
- Generating provenance metadata (sanitized command args, input hashes)
- Writing sidecar files (default: <output>.gremlin.meta.json or configured dir)
- Cleaning up old provenance files based on retention policy
"""

from __future__ import annotations

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

from g_gremlin import __version__
from g_gremlin.paths import gremlin_path

CONFIG_PATH = gremlin_path("config.json")
DEFAULT_RETENTION_DAYS = 30
DEFAULT_SCHEMA = "Provenance/v1"

# Keys that should be redacted from logs, provenance, and error messages
SECRET_KEYS = {
    # Generic patterns
    "token", "secret", "password", "api_key", "access_token",
    "refresh_token", "client_secret", "api_token", "bearer",
    "authorization", "credential", "apikey", "auth_token", "webhook",
    # Provider-specific (for extra safety)
    "apollo_api_key", "zoominfo_client_id", "zoominfo_client_secret",
    "hubspot_access_token", "zendesk_api_token", "apify_api_token",
    "commonroom_webhook_secret", "commonroom_api_key",
    "heyreach_api_key", "notion_token", "outline_api_token",
    "confluence_api_token",
}


def _now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


def _load_config() -> Dict[str, Any]:
    if not CONFIG_PATH.exists():
        return {}
    try:
        return json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
    except Exception:
        return {}


def sanitize_args(args: Dict[str, Any]) -> Dict[str, Any]:
    """Redact obvious secrets from command args."""
    redacted: Dict[str, Any] = {}
    for key, value in args.items():
        if any(secret_key in key.lower() for secret_key in SECRET_KEYS):
            redacted[key] = "[REDACTED]"
        else:
            redacted[key] = value
    return redacted


def _sha256_file(path: Path) -> Optional[str]:
    if not path.exists() or not path.is_file():
        return None
    h = hashlib.sha256()
    try:
        with path.open("rb") as f:
            for chunk in iter(lambda: f.read(8192), b""):
                h.update(chunk)
    except Exception:
        return None
    return h.hexdigest()


def collect_input_metadata(paths: Iterable[Path]) -> List[Dict[str, Any]]:
    entries: List[Dict[str, Any]] = []
    for p in paths:
        try:
            stat = p.stat()
        except FileNotFoundError:
            continue
        entries.append(
            {
                "path": str(p),
                "mtime": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(),
                "sha256": _sha256_file(p),
            }
        )
    return entries


def build_provenance(
    command: str,
    args: Dict[str, Any],
    inputs: Iterable[Path] | None = None,
    provider: Optional[str] = None,
) -> Dict[str, Any]:
    sanitized = sanitize_args(args)
    input_entries = collect_input_metadata(inputs or [])
    query_hash = hashlib.sha256(json.dumps(sanitized, sort_keys=True).encode("utf-8")).hexdigest()

    return {
        "$schema": DEFAULT_SCHEMA,
        "run_id": str(uuid.uuid4()),
        "timestamp": _now_iso(),
        "actor": f"g-gremlin/{__version__}",
        "provider": provider,
        "inputs": input_entries,
        "query_hash": query_hash,
        "command": command,
        "command_args_sanitized": sanitized,
    }


def _sidecar_path(output_path: Path, provenance_dir: Optional[Path]) -> Path:
    base_name = f"{output_path.name}.gremlin.meta.json"
    if provenance_dir:
        provenance_dir.mkdir(parents=True, exist_ok=True)
        return provenance_dir / base_name
    return output_path.with_suffix(output_path.suffix + ".gremlin.meta.json")


def write_sidecar(
    output_path: Path,
    provenance: Dict[str, Any],
    provenance_dir: Optional[Path] = None,
) -> Path:
    sidecar = _sidecar_path(output_path, provenance_dir)
    sidecar.parent.mkdir(parents=True, exist_ok=True)
    sidecar.write_text(json.dumps(provenance, indent=2), encoding="utf-8")
    return sidecar


def cleanup_provenance(
    path: Path,
    days: int = DEFAULT_RETENTION_DAYS,
    include_sidecars: bool = True,
) -> List[Path]:
    """
    Delete provenance files older than `days`. Sidecars are detected by
    *.gremlin.meta.json pattern when include_sidecars=True.
    """
    if not path.exists():
        return []

    cutoff = datetime.now(timezone.utc) - timedelta(days=days)
    removed: List[Path] = []

    patterns = ["*.gremlin.meta.json"] if include_sidecars else ["*.meta.json"]
    for pattern in patterns:
        for file_path in path.rglob(pattern):
            try:
                mtime = datetime.fromtimestamp(file_path.stat().st_mtime, tz=timezone.utc)
            except FileNotFoundError:
                continue
            if mtime < cutoff:
                try:
                    file_path.unlink()
                    removed.append(file_path)
                except Exception:
                    continue
    return removed


def default_provenance_dir(config: Optional[Dict[str, Any]] = None) -> Optional[Path]:
    cfg = config if config is not None else _load_config()
    path = cfg.get("provenance_dir")
    if path:
        return Path(path)
    # fallback: use .g_gremlin if it exists
    fallback = Path(".g_gremlin")
    if fallback.exists():
        return fallback
    return None


def default_retention_days(config: Optional[Dict[str, Any]] = None) -> int:
    cfg = config if config is not None else _load_config()
    return int(cfg.get("provenance_retention_days", DEFAULT_RETENTION_DAYS))
