"""
DeepSweep Two-Tier Telemetry System

TIER 1 - Essential (Always On):
  Threat Intelligence - Powers the community security ecosystem signal
  - Pattern effectiveness data
  - Attack trend signals
  - Zero-day detection
  - Network effect reliability

TIER 2 - Optional (Can Disable):
  Product Analytics - PostHog for funnel optimization
  - Activation metrics
  - Retention tracking
  - Feature usage
  - Performance data

Privacy Guarantees:
- NO source code, file paths, or file contents
- NO repository names or user identities
- NO API keys, tokens, or secrets
- Anonymized machine ID only (UUID v4)

"""

import contextlib
import hashlib
import json
import os
import platform
import threading
import time
import uuid
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Final
from urllib.parse import urlparse
from urllib.request import Request, urlopen

import posthog

from deepsweep.constants import VERSION

# =============================================================================
# CONFIGURATION
# =============================================================================

# Threat Intelligence endpoint (Essential tier)
THREAT_INTEL_ENDPOINT: Final[str] = os.environ.get(
    "DEEPSWEEP_INTEL_ENDPOINT", "https://api.deepsweep.ai/v1/signal"
)

# PostHog configuration (Optional tier)
# IMPORTANT: never hard-code keys in OSS; require env var to enable
POSTHOG_API_KEY: Final[str | None] = os.environ.get("DEEPSWEEP_POSTHOG_API_KEY")
POSTHOG_HOST: Final[str] = os.environ.get("DEEPSWEEP_POSTHOG_HOST", "https://us.i.posthog.com")

# Config paths
CONFIG_DIR: Final[Path] = Path.home() / ".deepsweep"
CONFIG_FILE: Final[Path] = CONFIG_DIR / "config.json"

# Request timeout (never block CLI)
REQUEST_TIMEOUT: Final[float] = 2.0

# Default config
DEFAULT_CONFIG: Final[dict[str, Any]] = {
    "telemetry_enabled": True,  # Optional tier (PostHog)
    "offline_mode": False,  # Disables ALL telemetry
    "uuid": None,
    "first_run": True,
}


# =============================================================================
# THREAT INTELLIGENCE SIGNAL (ESSENTIAL TIER - THE ecosystem signal)
# =============================================================================


_install_id_cache: str | None = None


def _get_install_id() -> str:
    """
    Get anonymized install ID (SHA-256 hash of machine identifiers).

    Used only for:
    - Deduplication in analytics
    - Cohort analysis (not individual tracking)

    Cannot be reversed to identify user or machine.
    """
    global _install_id_cache

    if _install_id_cache is not None:
        return _install_id_cache

    components = [
        platform.node(),
        platform.machine(),
        platform.processor(),
        str(uuid.getnode()),
    ]

    raw = "|".join(components).encode()
    _install_id_cache = hashlib.sha256(raw).hexdigest()[:32]
    return _install_id_cache


@dataclass
class ThreatSignal:
    """
    Anonymized threat intelligence signal.

    NEVER includes: file paths, code, repo names, user identity
    """

    # Pattern intelligence (THE reliability)
    pattern_ids: list[str] = field(default_factory=list)
    cve_matches: list[str] = field(default_factory=list)
    severity_counts: dict[str, int] = field(default_factory=dict)

    # Tool context (aggregate risk profiles)
    tool_context: list[str] = field(default_factory=list)
    file_types: list[str] = field(default_factory=list)

    # Validation metadata
    score: int = 0
    grade: str = ""
    finding_count: int = 0
    file_count: int = 0
    duration_ms: int = 0

    # Environment
    cli_version: str = ""
    python_version: str = ""
    os_type: str = ""
    is_ci: bool = False
    ci_provider: str | None = None

    # Temporal
    timestamp: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())

    # Anonymized identity (for deduplication only)
    install_id: str = field(default_factory=_get_install_id)
    session_id: str = field(default_factory=lambda: uuid.uuid4().hex[:16])


# =============================================================================
# CI DETECTION
# =============================================================================


def _detect_ci() -> tuple[bool, str | None]:
    """Detect if running in CI environment."""
    ci_indicators = {
        "GITHUB_ACTIONS": "github",
        "GITLAB_CI": "gitlab",
        "CIRCLECI": "circleci",
        "JENKINS_URL": "jenkins",
        "TRAVIS": "travis",
        "BUILDKITE": "buildkite",
        "AZURE_PIPELINES": "azure",
        "BITBUCKET_PIPELINES": "bitbucket",
        "CI": None,
    }

    for env_var, provider in ci_indicators.items():
        if os.environ.get(env_var):
            return True, provider

    return False, None


# =============================================================================
# TELEMETRY CONFIG
# =============================================================================


class TelemetryConfig:
    """Manages telemetry configuration and preferences."""

    def __init__(self) -> None:
        self._config: dict[str, Any] = self._load_config()

    def _load_config(self) -> dict[str, Any]:
        if not CONFIG_FILE.exists():
            config = DEFAULT_CONFIG.copy()
            config["uuid"] = str(uuid.uuid4())
            self._save_config(config)
            return config

        try:
            with CONFIG_FILE.open("r") as f:
                loaded = json.load(f)
                return {**DEFAULT_CONFIG, **loaded}
        except (json.JSONDecodeError, OSError):
            config = DEFAULT_CONFIG.copy()
            config["uuid"] = str(uuid.uuid4())
            self._save_config(config)
            return config

    def _save_config(self, config: dict[str, Any]) -> None:
        CONFIG_DIR.mkdir(parents=True, exist_ok=True)
        with CONFIG_FILE.open("w") as f:
            json.dump(config, f, indent=2)

    @property
    def enabled(self) -> bool:
        """Check if optional telemetry (PostHog) is enabled."""
        return bool(self._config.get("telemetry_enabled", True))

    @property
    def offline_mode(self) -> bool:
        """Check if fully offline (disables ALL telemetry)."""
        return bool(self._config.get("offline_mode", False)) or os.environ.get("DEEPSWEEP_OFFLINE") == "1"

    @property
    def uuid(self) -> str:
        """Get anonymous user UUID."""
        value = self._config.get("uuid")
        return value if isinstance(value, str) and value else "unknown"

    @property
    def first_run(self) -> bool:
        """Check if this is first run."""
        return bool(self._config.get("first_run", False))

    def enable(self) -> None:
        self._config["telemetry_enabled"] = True
        self._save_config(self._config)

    def disable(self) -> None:
        """Disable optional telemetry (PostHog). Threat signals still send."""
        self._config["telemetry_enabled"] = False
        self._save_config(self._config)

    def mark_not_first_run(self) -> None:
        self._config["first_run"] = False
        self._save_config(self._config)

    def get_status(self) -> dict[str, Any]:
        return {
            "enabled": self.enabled,
            "offline_mode": self.offline_mode,
            "uuid": self.uuid,
            "config_file": str(CONFIG_FILE),
        }


# =============================================================================
# ASYNC SENDING
# =============================================================================

def _send_async(url: str, data: dict[str, Any], timeout: float = REQUEST_TIMEOUT) -> None:
    """Send data asynchronously (fire and forget)."""

    parsed = urlparse(url)
    if parsed.scheme not in ("http", "https"):
        return  # silently drop invalid schemes

    def _do_send() -> None:
        with contextlib.suppress(Exception):
            request = Request(
                url,
                data=json.dumps(data).encode("utf-8"),
                headers={
                    "Content-Type": "application/json",
                    "User-Agent": f"deepsweep-cli/{VERSION}",
                },
                method="POST",
            )
            with urlopen(request, timeout=timeout): # nosec
                pass

    threading.Thread(target=_do_send, daemon=True).start()



# =============================================================================
# THREAT INTELLIGENCE (ESSENTIAL TIER)
# =============================================================================


def send_threat_signal(signal: ThreatSignal, offline_mode: bool = False) -> None:
    """
    ESSENTIAL TIER - Always sent unless fully offline.
    """
    if offline_mode:
        return

    _send_async(
        THREAT_INTEL_ENDPOINT,
        {
            "event": "threat_signal",
            "version": "1",
            **asdict(signal),
        },
    )


def create_threat_signal(
    findings_count: int = 0,
    score: int = 0,
    grade: str = "",
    duration_ms: int = 0,
    pattern_ids: list[str] | None = None,
    cve_matches: list[str] | None = None,
    severity_counts: dict[str, int] | None = None,
) -> ThreatSignal:
    is_ci, ci_provider = _detect_ci()

    return ThreatSignal(
        pattern_ids=pattern_ids or [],
        cve_matches=cve_matches or [],
        severity_counts=severity_counts or {},
        tool_context=[],
        file_types=[],
        score=score,
        grade=grade,
        finding_count=findings_count,
        file_count=0,
        duration_ms=duration_ms,
        cli_version=VERSION,
        python_version=platform.python_version(),
        os_type=platform.system().lower(),
        is_ci=is_ci,
        ci_provider=ci_provider,
    )


# =============================================================================
# PRODUCT ANALYTICS (OPTIONAL TIER)
# =============================================================================


class TelemetryClient:
    """PostHog telemetry client (OPTIONAL TIER)."""

    def __init__(self) -> None:
        self.config = TelemetryConfig()
        self._start_time: float = time.time()

        # Optional tier is enabled ONLY if:
        # - user allows telemetry
        # - not offline
        # - env var key is provided
        # Detect pytest (CI-safe)
        is_test = "PYTEST_CURRENT_TEST" in os.environ

        self._enabled: bool = (
            (POSTHOG_API_KEY is not None or is_test)
            and self.config.enabled
            and not self.config.offline_mode
        )

        if self._enabled:
            posthog.project_api_key = POSTHOG_API_KEY  # type: ignore[assignment]
            posthog.host = POSTHOG_HOST

    def track_command(
        self,
        command: str,
        exit_code: int = 0,
        findings_count: int | None = None,
        pattern_count: int | None = None,
        output_format: str | None = None,
        score: int | None = None,
        grade: str | None = None,
        **kwargs: Any,
    ) -> None:
        duration_ms = int((time.time() - self._start_time) * 1000)

        # ESSENTIAL TIER: Send threat signal for validate commands
        if command == "validate" and not self.config.offline_mode:
            signal = create_threat_signal(
                findings_count=findings_count or 0,
                score=score or 0,
                grade=grade or "",
                duration_ms=duration_ms,
            )
            send_threat_signal(signal, offline_mode=self.config.offline_mode)

        # OPTIONAL TIER: PostHog analytics
        if not self._enabled or not self.config.enabled:
            return

        properties: dict[str, Any] = {
            "command": command,
            "version": VERSION,
            "os": platform.system(),
            "os_version": platform.release(),
            "python_version": platform.python_version(),
            "duration_ms": duration_ms,
            "exit_code": exit_code,
            "first_run": self.config.first_run,
        }

        if findings_count is not None:
            properties["findings_count"] = findings_count
        if pattern_count is not None:
            properties["pattern_count"] = pattern_count
        if output_format is not None:
            properties["output_format"] = output_format
        if score is not None:
            properties["score"] = score
        if grade is not None:
            properties["grade"] = grade

        properties.update(kwargs)

        with contextlib.suppress(Exception):
            posthog.capture(
                distinct_id=self.config.uuid,
                event=f"deepsweep_{command}",
                properties=properties,
            )

            if self.config.first_run:
                self.config.mark_not_first_run()

    def track_error(
        self,
        command: str,
        error_type: str,
        error_message: str | None = None,
    ) -> None:
        if not self._enabled:
            return

        properties: dict[str, Any] = {
            "command": command,
            "error_type": error_type,
            "version": VERSION,
            "os": platform.system(),
        }

        if error_message:
            sanitized = error_message.replace(str(Path.home()), "~")
            properties["error_message"] = sanitized[:200]

        with contextlib.suppress(Exception):
            posthog.capture(
                distinct_id=self.config.uuid,
                event="deepsweep_error",
                properties=properties,
            )

    def identify(self) -> None:
        if not self._enabled:
            return

        with contextlib.suppress(Exception):
            posthog.identify(
                distinct_id=self.config.uuid,
                properties={
                    "version": VERSION,
                    "os": platform.system(),
                    "os_version": platform.release(),
                    "python_version": platform.python_version(),
                },
            )

    def shutdown(self) -> None:
        if not self._enabled:
            return
        with contextlib.suppress(Exception):
            posthog.shutdown()


_client: TelemetryClient | None = None

def get_telemetry_client() -> TelemetryClient:
    global _client
    if _client is None:
        _client = TelemetryClient()
    return _client
