"""Shared fixtures for E2E tests.

Provides Docker Compose environment management, service health checks,
and GCP client fixtures that parse real OTel Collector exports.
"""

import os
import time
import subprocess
import pytest
import requests
import json
from pathlib import Path
from typing import Generator, Dict, Any, List

# Configuration
DOCKER_COMPOSE_FILE = Path(__file__).parent.parent.parent / "docker-compose.yml"
E2E_DOCKER_COMPOSE_FILE = Path(__file__).parent / "docker-compose.e2e.yml"
COLLECTOR_HEALTH_URL = "http://localhost:13133/"
COLLECTOR_OTLP_ENDPOINT = "http://localhost:4318"
TEST_EXPORTS_DIR = DOCKER_COMPOSE_FILE.parent / "test-exports"
MAX_STARTUP_WAIT = 60  # seconds
SERVICE_CHECK_INTERVAL = 2  # seconds


@pytest.fixture(scope="session")
def docker_compose_environment() -> Generator[Dict[str, str], None, None]:
    """Start Docker Compose environment for E2E tests.

    Starts OTel Collector and optionally GCP mock services.
    Waits for services to be healthy before yielding.
    Tears down services after test session completes.

    Yields:
        Dict with endpoint URLs for services
    """
    # Check if E2E compose file exists, otherwise use main compose
    compose_files = [str(DOCKER_COMPOSE_FILE)]
    if E2E_DOCKER_COMPOSE_FILE.exists():
        compose_files.append(str(E2E_DOCKER_COMPOSE_FILE))

    # Build docker-compose command (try both 'docker compose' and 'docker-compose')
    try:
        # Try modern 'docker compose' first
        subprocess.run(["docker", "compose", "version"], check=True, capture_output=True)
        compose_cmd = ["docker", "compose"]
    except (subprocess.CalledProcessError, FileNotFoundError):
        # Fall back to legacy 'docker-compose'
        compose_cmd = ["docker-compose"]

    # Build full command with compose files
    full_cmd = compose_cmd.copy()
    for file in compose_files:
        full_cmd.extend(["-f", file])

    # Ensure test-exports directory exists with correct permissions
    TEST_EXPORTS_DIR.mkdir(exist_ok=True)
    os.chmod(TEST_EXPORTS_DIR, 0o777)  # Ensure container can write

    # Clean up old export files
    for export_file in TEST_EXPORTS_DIR.glob("*.json"):
        try:
            export_file.unlink()
        except Exception:
            pass

    # Start services
    print("\n🚀 Starting Docker Compose services for E2E tests...")
    subprocess.run(
        full_cmd + ["up", "-d", "otel-collector"],
        check=True,
        cwd=DOCKER_COMPOSE_FILE.parent,
    )

    # Wait for services to be healthy
    print("⏳ Waiting for services to be healthy...")
    start_time = time.time()
    while time.time() - start_time < MAX_STARTUP_WAIT:
        try:
            response = requests.get(COLLECTOR_HEALTH_URL, timeout=2)
            if response.status_code == 200:
                print("✅ OTel Collector is healthy")
                break
        except requests.RequestException:
            pass
        time.sleep(SERVICE_CHECK_INTERVAL)
    else:
        # Cleanup on failure
        subprocess.run(full_cmd + ["down"], cwd=DOCKER_COMPOSE_FILE.parent)
        pytest.fail("OTel Collector failed to become healthy within timeout")

    # Yield endpoints
    endpoints = {
        "collector_otlp": COLLECTOR_OTLP_ENDPOINT,
        "collector_health": COLLECTOR_HEALTH_URL,
        "test_exports_dir": str(TEST_EXPORTS_DIR),
    }

    yield endpoints

    # Teardown: Stop services
    print("\n🧹 Stopping Docker Compose services...")
    subprocess.run(full_cmd + ["down"], cwd=DOCKER_COMPOSE_FILE.parent, check=False)


@pytest.fixture
def wait_for_telemetry():
    """Fixture providing utility to wait for telemetry propagation.

    Returns:
        Callable that waits for specified duration with progress indicator
    """

    def _wait(seconds: float, reason: str = "telemetry propagation"):
        """Wait for telemetry to propagate through pipeline.

        Args:
            seconds: Number of seconds to wait
            reason: Description of what we're waiting for
        """
        print(f"⏳ Waiting {seconds}s for {reason}...")
        time.sleep(seconds)
        print("✅ Wait complete")

    return _wait


class RealMockMonitoringClient:
    """GCP Cloud Monitoring mock client that parses real OTel Collector exports.

    This reads from the file exporter output in test-exports/ to validate
    that metrics actually flowed through the entire pipeline.
    """

    def __init__(self, exports_dir: Path):
        self.exports_dir = Path(exports_dir)
        self.metrics_file = self.exports_dir / "otel-metrics.json"

    def query_metrics(
        self, project_id: str, metric_type: str, start_time: float, end_time: float
    ) -> List[Dict[str, Any]]:
        """Query metrics from parsed OTel Collector exports.

        Args:
            project_id: GCP project ID (unused in mock)
            metric_type: Metric name to filter by
            start_time: Query start time (unix timestamp)
            end_time: Query end time (unix timestamp)

        Returns:
            List of metric data points matching the query
        """
        if not self.metrics_file.exists():
            return []

        metrics = []
        try:
            with open(self.metrics_file, "r") as f:
                # File exporter writes JSONL format (one JSON object per line)
                for line in f:
                    if not line.strip():
                        continue
                    try:
                        data = json.loads(line)
                        # Parse OTLP JSON format
                        metrics.extend(
                            self._parse_otlp_metrics(data, metric_type, start_time, end_time)
                        )
                    except json.JSONDecodeError:
                        continue
        except Exception as e:
            print(f"Warning: Failed to parse metrics file: {e}")

        return metrics

    def _parse_otlp_metrics(
        self, data: Dict, metric_type: str, start_time: float, end_time: float
    ) -> List[Dict]:
        """Parse OTLP JSON format metrics."""
        results = []

        # OTLP format: { "resourceMetrics": [...] }
        for resource_metric in data.get("resourceMetrics", []):
            for scope_metric in resource_metric.get("scopeMetrics", []):
                for metric in scope_metric.get("metrics", []):
                    name = metric.get("name", "")

                    # Filter by metric name
                    if metric_type and metric_type not in name:
                        continue

                    # Extract data points based on metric type
                    data_points = []
                    if "sum" in metric:
                        data_points = metric["sum"].get("dataPoints", [])
                    elif "gauge" in metric:
                        data_points = metric["gauge"].get("dataPoints", [])
                    elif "histogram" in metric:
                        data_points = metric["histogram"].get("dataPoints", [])

                    for dp in data_points:
                        # Check timestamp (convert nanoseconds to seconds)
                        ts_ns = int(dp.get("timeUnixNano", 0))
                        ts = ts_ns / 1_000_000_000 if ts_ns else time.time()

                        if not (start_time <= ts <= end_time):
                            continue

                        # Extract attributes
                        attributes = {}
                        for attr in dp.get("attributes", []):
                            key = attr.get("key")
                            value = attr.get("value", {})
                            # Extract value based on type
                            if "stringValue" in value:
                                attributes[key] = value["stringValue"]
                            elif "intValue" in value:
                                attributes[key] = int(value["intValue"])
                            elif "doubleValue" in value:
                                attributes[key] = float(value["doubleValue"])
                            elif "boolValue" in value:
                                attributes[key] = value["boolValue"]

                        # Build result
                        result = {
                            "metric_type": name,
                            "attributes": attributes,
                            "timestamp": ts,
                        }

                        # Add value based on metric type
                        if "asInt" in dp:
                            result["value"] = int(dp["asInt"])
                        elif "asDouble" in dp:
                            result["value"] = float(dp["asDouble"])
                        elif "count" in dp:  # Histogram
                            result["count"] = int(dp.get("count", 0))
                            result["sum"] = float(dp.get("sum", 0.0))
                            result["bucketCounts"] = dp.get("bucketCounts", [])
                            result["explicitBounds"] = dp.get("explicitBounds", [])
                            # Calculate value as average for histogram
                            if result["count"] > 0:
                                result["value"] = result["sum"] / result["count"]

                        results.append(result)

        return results


class RealMockLoggingClient:
    """GCP Cloud Logging mock client that parses real OTel Collector exports."""

    def __init__(self, exports_dir: Path):
        self.exports_dir = Path(exports_dir)
        self.logs_file = self.exports_dir / "otel-logs.json"

    def query_logs(
        self, project_id: str, filter_str: str, start_time: float, end_time: float
    ) -> List[Dict[str, Any]]:
        """Query logs from parsed OTel Collector exports."""
        if not self.logs_file.exists():
            return []

        logs = []
        try:
            with open(self.logs_file, "r") as f:
                for line in f:
                    if not line.strip():
                        continue
                    try:
                        data = json.loads(line)
                        logs.extend(self._parse_otlp_logs(data, start_time, end_time))
                    except json.JSONDecodeError:
                        continue
        except Exception as e:
            print(f"Warning: Failed to parse logs file: {e}")

        return logs

    def _parse_otlp_logs(self, data: Dict, start_time: float, end_time: float) -> List[Dict]:
        """Parse OTLP JSON format logs."""
        results = []

        for resource_log in data.get("resourceLogs", []):
            for scope_log in resource_log.get("scopeLogs", []):
                for log_record in scope_log.get("logRecords", []):
                    # Check timestamp
                    ts_ns = int(log_record.get("timeUnixNano", 0))
                    ts = ts_ns / 1_000_000_000 if ts_ns else time.time()

                    if not (start_time <= ts <= end_time):
                        continue

                    # Extract attributes
                    attributes = {}
                    for attr in log_record.get("attributes", []):
                        key = attr.get("key")
                        value = attr.get("value", {})
                        if "stringValue" in value:
                            attributes[key] = value["stringValue"]
                        elif "intValue" in value:
                            attributes[key] = int(value["intValue"])
                        elif "doubleValue" in value:
                            attributes[key] = float(value["doubleValue"])

                    # Build result
                    result = {
                        "severity": log_record.get("severityText", "INFO"),
                        "message": log_record.get("body", {}).get("stringValue", ""),
                        "attributes": attributes,
                        "timestamp": ts,
                    }

                    results.append(result)

        return results


class RealMockTraceClient:
    """GCP Cloud Trace mock client that parses real OTel Collector exports."""

    def __init__(self, exports_dir: Path):
        self.exports_dir = Path(exports_dir)
        self.traces_file = self.exports_dir / "otel-traces.json"

    def get_trace(self, project_id: str, trace_id: str) -> Dict[str, Any]:
        """Get trace by ID from parsed OTel Collector exports."""
        if not self.traces_file.exists():
            return None

        try:
            with open(self.traces_file, "r") as f:
                for line in f:
                    if not line.strip():
                        continue
                    try:
                        data = json.loads(line)
                        trace = self._find_trace_by_id(data, trace_id)
                        if trace:
                            return trace
                    except json.JSONDecodeError:
                        continue
        except Exception as e:
            print(f"Warning: Failed to parse traces file: {e}")

        return None

    def _find_trace_by_id(self, data: Dict, trace_id: str) -> Dict:
        """Find trace by ID in OTLP JSON format."""
        spans = []

        for resource_span in data.get("resourceSpans", []):
            for scope_span in resource_span.get("scopeSpans", []):
                for span in scope_span.get("spans", []):
                    span_trace_id = span.get("traceId", "")

                    # Compare trace IDs (handle hex formatting)
                    if (
                        span_trace_id.lower() == trace_id.lower()
                        or span_trace_id.lower() == trace_id.lower().zfill(32)
                    ):
                        # Extract span attributes
                        attributes = {}
                        for attr in span.get("attributes", []):
                            key = attr.get("key")
                            value = attr.get("value", {})
                            if "stringValue" in value:
                                attributes[key] = value["stringValue"]
                            elif "intValue" in value:
                                attributes[key] = int(value["intValue"])
                            elif "doubleValue" in value:
                                attributes[key] = float(value["doubleValue"])
                            elif "boolValue" in value:
                                attributes[key] = value["boolValue"]

                        # Calculate duration
                        start_ns = int(span.get("startTimeUnixNano", 0))
                        end_ns = int(span.get("endTimeUnixNano", 0))
                        duration_seconds = (
                            (end_ns - start_ns) / 1_000_000_000 if end_ns > start_ns else 0
                        )

                        spans.append(
                            {
                                "span_id": span.get("spanId"),
                                "parent_span_id": span.get("parentSpanId"),
                                "name": span.get("name"),
                                "attributes": attributes,
                                "duration_seconds": duration_seconds,
                                "start_time": start_ns / 1_000_000_000,
                                "end_time": end_ns / 1_000_000_000,
                            }
                        )

        if spans:
            return {
                "trace_id": trace_id,
                "spans": spans,
                "timestamp": spans[0]["start_time"] if spans else time.time(),
            }

        return None


@pytest.fixture
def gcp_monitoring_client(docker_compose_environment):
    """Fixture for GCP Cloud Monitoring client that parses real exports."""
    exports_dir = docker_compose_environment["test_exports_dir"]
    return RealMockMonitoringClient(exports_dir)


@pytest.fixture
def gcp_logging_client(docker_compose_environment):
    """Fixture for GCP Cloud Logging client that parses real exports."""
    exports_dir = docker_compose_environment["test_exports_dir"]
    return RealMockLoggingClient(exports_dir)


@pytest.fixture
def gcp_trace_client(docker_compose_environment):
    """Fixture for GCP Cloud Trace client that parses real exports."""
    exports_dir = docker_compose_environment["test_exports_dir"]
    return RealMockTraceClient(exports_dir)


@pytest.fixture
def gcp_project_id() -> str:
    """GCP project ID for E2E tests."""
    return os.getenv("GCP_PROJECT_ID", "test-project-12345")


@pytest.fixture
def cleanup_collector_exports(docker_compose_environment):
    """Fixture to clean up collector export files after tests."""
    yield

    # Cleanup test-exports directory
    exports_dir = Path(docker_compose_environment["test_exports_dir"])
    if exports_dir.exists():
        for file in exports_dir.glob("*.json"):
            try:
                file.unlink()
            except Exception:
                pass
