# Copyright (c) 2025 CloudZero - ALL RIGHTS RESERVED - PROPRIETARY AND CONFIDENTIAL
# Unauthorized copying of this file and/or project, via any medium is strictly prohibited.
# Direct all questions to legal@cloudzero.com
#

import json
import os
import re
from collections import defaultdict, deque
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any

from src.constants import CZOPS_WORKING_DIR
from src.utils.core import remove_nones


class ApiRecorder:
    """Records API requests and responses for later schema generation."""

    MAX_STORED_RECORDS = 5
    # Patterns for sensitive data that should be obfuscated
    SENSITIVE_PATTERNS = [
        (
            re.compile(r'(api[_-]?key|apikey|auth[_-]?token|password|secret)["\']?\s*[:=]\s*["\']([^"\']+)["\']', re.IGNORECASE),
            r'\1": "***REDACTED***"',
        ),
        (re.compile(r"(bearer\s+)([a-zA-Z0-9._-]+)", re.IGNORECASE), r"\1***REDACTED***"),
        (re.compile(r"(eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,})", re.IGNORECASE), r"***REDACTED-JWT***"),
        (re.compile(r"([A-Za-z0-9+/]{40,}[=]{0,2})"), r"***REDACTED-BASE64***"),
    ]
    # Timestamp field for tracking record creation time
    TIMESTAMP_FIELD = "recorded_at"

    def __init__(self):
        """Initialize the API recorder."""
        self.records_dir = Path(os.path.expanduser(CZOPS_WORKING_DIR)) / "api_records"
        self.records_dir.mkdir(parents=True, exist_ok=True)
        self.in_memory_records = defaultdict(lambda: deque(maxlen=self.MAX_STORED_RECORDS))
        self._load_existing_records()

    def _load_existing_records(self):
        """Load existing records from disk into memory."""
        for file_path in self.records_dir.glob("*.json"):
            try:
                with open(file_path) as f:
                    data = json.load(f)
                    endpoint = file_path.stem
                    # Convert list to deque with max length
                    records = deque(data, maxlen=self.MAX_STORED_RECORDS)
                    self.in_memory_records[endpoint] = records
            except (OSError, json.JSONDecodeError) as e:
                print(f"Error loading API records from {file_path}: {e}")

    def _normalize_endpoint(self, api_path: str) -> str:
        """
        Normalize the endpoint path to create a consistent key.

        For paths with IDs like /organizations/123, we convert to /organizations/{id}
        """
        parts = api_path.strip("/").split("/")
        normalized_parts = []

        for i, part in enumerate(parts):
            # If this segment appears to be an ID and follows a collection name, use a placeholder
            if i > 0 and (
                part.isdigit()  # AWS account IDs, numeric IDs
                or (len(part) == 36 and "-" in part)
            ):  # UUIDs
                normalized_parts.append("{id}")
            else:
                normalized_parts.append(part)

        return "_".join(normalized_parts)

    def _obfuscate_sensitive_data(self, data: Any) -> Any:
        """
        Obfuscate sensitive data like API keys, passwords, and tokens.

        Args:
            data: The data to obfuscate

        Returns:
            The obfuscated data
        """
        if data is None:
            return None

        if isinstance(data, dict):
            obfuscated_data = {}
            for key, value in data.items():
                # Check for common sensitive keys
                if any(
                    sensitive_key in key.lower()
                    for sensitive_key in [
                        "api_key",
                        "apikey",
                        "token",
                        "password",
                        "secret",
                        "auth",
                        "credential",
                        "private",
                        "access_key",
                        "access_token",
                        "x-api-key",
                        "external_id",
                    ]
                ) and isinstance(value, str):
                    obfuscated_data[key] = "***REDACTED***"
                else:
                    obfuscated_data[key] = self._obfuscate_sensitive_data(value)
            return obfuscated_data
        elif isinstance(data, list):
            return [self._obfuscate_sensitive_data(item) for item in data]
        elif isinstance(data, str):
            # Apply regex patterns to obfuscate sensitive data in strings
            obfuscated_str = data
            for pattern, replacement in self.SENSITIVE_PATTERNS:
                obfuscated_str = pattern.sub(replacement, obfuscated_str)
            return obfuscated_str
        else:
            return data

    def record(
        self,
        request_type: str,
        api_path: str,
        payload: Any | None = None,
        params: dict[str, Any] | None = None,
        headers: dict[str, str] | None = None,
        response: dict[str, Any] | None = None,
    ) -> None:
        """
        Record an API request and response.

        Args:
            request_type: HTTP method (get, post, put, etc.)
            api_path: The API endpoint path
            payload: The request payload/body
            params: The query parameters
            headers: The HTTP headers sent with the request
            response: The API response
        """
        endpoint_key = f"{request_type}_{self._normalize_endpoint(api_path)}"

        # Obfuscate sensitive data before storing
        obfuscated_payload = self._obfuscate_sensitive_data(payload)
        obfuscated_params = self._obfuscate_sensitive_data(params)
        obfuscated_headers = self._obfuscate_sensitive_data(headers)
        obfuscated_response = self._obfuscate_sensitive_data(response)

        record = {
            "request": {
                "method": request_type,
                "path": api_path,
                "payload": obfuscated_payload,
                "params": obfuscated_params,
                "headers": obfuscated_headers,
            },
            "response": obfuscated_response,
            self.TIMESTAMP_FIELD: datetime.now().isoformat(),
        }

        # Remove None values for cleaner storage
        record = remove_nones(record)

        # Add to in-memory records
        self.in_memory_records[endpoint_key].append(record)

        # Save to disk
        file_path = self.records_dir / f"{endpoint_key}.json"
        try:
            with open(file_path, "w") as f:
                # Convert deque to list for serialization
                json.dump(list(self.in_memory_records[endpoint_key]), f, indent=2, default=str)
        except (OSError, TypeError) as e:
            print(f"Error saving API record to {file_path}: {e}")

    def get_all_records(self) -> dict[str, list[dict[str, Any]]]:
        """
        Get all recorded API data.

        Returns:
            Dict mapping endpoint keys to lists of request/response records
        """
        result = {}
        for endpoint, records in self.in_memory_records.items():
            result[endpoint] = list(records)
        return result

    def get_records_for_endpoint(self, endpoint: str) -> list[dict[str, Any]]:
        """
        Get recorded data for a specific endpoint.

        Args:
            endpoint: The normalized endpoint key

        Returns:
            List of request/response records for that endpoint
        """
        return list(self.in_memory_records.get(endpoint, []))

    def purge_old_records(self, days: int) -> tuple[int, int]:
        """
        Purge API records older than the specified number of days.

        Args:
            days: Number of days; records older than this will be purged

        Returns:
            Tuple of (number of endpoints affected, total records purged)
        """
        if days <= 0:
            raise ValueError("Days must be a positive number")

        cutoff_date = datetime.now() - timedelta(days=days)
        endpoints_affected = 0
        total_records_purged = 0

        # For each endpoint, filter out records older than the cutoff date
        for endpoint, records in self.in_memory_records.items():
            original_count = len(records)

            # Filter records and keep only those newer than the cutoff date
            # Records without a timestamp field are preserved
            filtered_records = []
            for record in records:
                if self.TIMESTAMP_FIELD not in record:
                    # Keep records with no timestamp
                    filtered_records.append(record)
                elif datetime.fromisoformat(record[self.TIMESTAMP_FIELD]) >= cutoff_date:
                    # Keep records newer than cutoff date
                    filtered_records.append(record)

            # Count purged records for this endpoint
            records_purged = original_count - len(filtered_records)

            # If any records were purged, update in-memory and on-disk
            if records_purged > 0:
                endpoints_affected += 1
                total_records_purged += records_purged

                # Update in-memory records
                self.in_memory_records[endpoint] = deque(filtered_records, maxlen=self.MAX_STORED_RECORDS)

                # Update on-disk records
                file_path = self.records_dir / f"{endpoint}.json"
                try:
                    # If no records left, delete the file
                    if not filtered_records:
                        if file_path.exists():
                            file_path.unlink()
                    else:
                        # Otherwise, write the filtered records
                        with open(file_path, "w") as f:
                            json.dump(filtered_records, f, indent=2, default=str)
                except (OSError, TypeError) as e:
                    print(f"Error purging API records for {endpoint}: {e}")

        return endpoints_affected, total_records_purged


# Singleton instance
recorder = ApiRecorder()
