"""JSON data analysis and processing utilities for LSCSIM."""

from __future__ import annotations

import json
import operator
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from pytola_sim.lscsim.utils.logger import get_logger

logger = get_logger(__name__)


@dataclass
class JsonAnalysisResult:
    """Result of JSON data analysis."""

    file_path: Path
    is_valid: bool
    structure_depth: int
    total_objects: int
    total_arrays: int
    total_primitives: int
    key_statistics: dict[str, int]
    size_bytes: int
    analysis_date: datetime = field(default_factory=datetime.now)
    errors: list[str] = field(default_factory=list)

    def __post_init__(self) -> None:
        """Post-initialization hook for result setup."""
        if self.errors is None:
            self.errors = []

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary."""
        return {
            "file_path": str(self.file_path),
            "is_valid": self.is_valid,
            "structure_depth": self.structure_depth,
            "total_objects": self.total_objects,
            "total_arrays": self.total_arrays,
            "total_primitives": self.total_primitives,
            "key_statistics": self.key_statistics,
            "size_bytes": self.size_bytes,
            "analysis_date": self.analysis_date.isoformat(),
            "errors": self.errors,
        }


class JsonAnalyzer:
    """Analyzer for JSON data structures."""

    def __init__(self) -> None:
        self._analyzed_files: list[JsonAnalysisResult] = []
        logger.info("JSON analyzer initialized")

    def analyze_file(self, file_path: Path) -> JsonAnalysisResult | None:
        """Analyze a JSON file."""
        try:
            if not file_path.exists():
                logger.error(f"File not found: {file_path}")
                return None

            # Get file size
            size_bytes = file_path.stat().st_size

            # Parse JSON
            with Path(file_path).open(encoding="utf-8") as f:
                try:
                    data = json.load(f)
                    is_valid = True
                    errors = []
                except json.JSONDecodeError as e:
                    logger.exception(f"Invalid JSON in {file_path}")
                    is_valid = False
                    errors = [f"JSON decode error: {e}"]
                    data = {}

            # Analyze structure
            structure_depth = self._calculate_depth(data)
            objects_count, arrays_count, primitives_count = self._count_elements(data)
            key_stats = self._analyze_keys(data)

            result = JsonAnalysisResult(
                file_path=file_path,
                is_valid=is_valid,
                structure_depth=structure_depth,
                total_objects=objects_count,
                total_arrays=arrays_count,
                total_primitives=primitives_count,
                key_statistics=key_stats,
                size_bytes=size_bytes,
                errors=errors,
            )

            self._analyzed_files.append(result)
            logger.info(f"Analyzed JSON file: {file_path.name}")
        except (OSError, ValueError, RuntimeError):
            logger.exception(f"Failed to analyze {file_path}")
            return None
        else:
            return result

    def analyze_directory(
        self,
        directory: Path,
        pattern: str = "*.json",
    ) -> list[JsonAnalysisResult]:
        """Analyze all JSON files in a directory."""
        results = []

        if not directory.exists() or not directory.is_dir():
            logger.error(f"Directory not found: {directory}")
            return results

        try:
            for file_path in directory.glob(pattern):
                result = self.analyze_file(file_path)
                if result:
                    results.append(result)

            logger.info(f"Analyzed {len(results)} JSON files in {directory}")
        except (OSError, ValueError, RuntimeError):
            logger.exception(f"Failed to analyze directory {directory}")
            return results
        else:
            return results

    def get_analysis_summary(self) -> dict[str, Any]:
        """Get summary of all analyses."""
        if not self._analyzed_files:
            return {"message": "No files analyzed yet"}

        valid_files = [f for f in self._analyzed_files if f.is_valid]
        invalid_files = [f for f in self._analyzed_files if not f.is_valid]

        # Calculate statistics
        total_size = sum(f.size_bytes for f in self._analyzed_files)
        avg_depth = sum(f.structure_depth for f in valid_files) / len(valid_files) if valid_files else 0
        avg_objects = sum(f.total_objects for f in valid_files) / len(valid_files) if valid_files else 0

        # Key frequency analysis
        all_keys = {}
        for result in valid_files:
            for key, count in result.key_statistics.items():
                all_keys[key] = all_keys.get(key, 0) + count

        # Top 10 most frequent keys
        top_keys = sorted(all_keys.items(), key=operator.itemgetter(1), reverse=True)[:10]

        return {
            "total_files_analyzed": len(self._analyzed_files),
            "valid_files": len(valid_files),
            "invalid_files": len(invalid_files),
            "total_size_bytes": total_size,
            "average_structure_depth": round(avg_depth, 2),
            "average_objects_per_file": round(avg_objects, 2),
            "most_frequent_keys": [{"key": k, "frequency": v} for k, v in top_keys],
            "largest_file": max(
                self._analyzed_files,
                key=lambda x: x.size_bytes,
            ).file_path.name
            if self._analyzed_files
            else None,
        }

    def export_analysis_report(self, output_path: Path) -> bool:
        """Export analysis results to a report file."""
        try:
            report_data = {
                "generated_date": datetime.now(timezone.utc).isoformat(),
                "summary": self.get_analysis_summary(),
                "detailed_results": [result.to_dict() for result in self._analyzed_files],
            }

            with Path(output_path).open("w", encoding="utf-8") as f:
                json.dump(report_data, f, indent=2, ensure_ascii=False)

            logger.info(f"Analysis report exported to: {output_path}")
        except (OSError, ValueError, RuntimeError):
            logger.exception("Failed to export analysis report")
            return False
        else:
            return True

    def _calculate_depth(self, obj: object) -> int:
        """Calculate maximum nesting depth of JSON structure."""
        if isinstance(obj, dict):
            if not obj:
                return 1
            return 1 + max(self._calculate_depth(v) for v in obj.values())
        if isinstance(obj, list):
            if not obj:
                return 1
            return 1 + max(self._calculate_depth(item) for item in obj)
        return 1

    def _count_elements(self, obj: object) -> tuple[int, int, int]:
        """Count objects, arrays, and primitive values."""
        objects_count = 0
        arrays_count = 0
        primitives_count = 0

        if isinstance(obj, dict):
            objects_count += 1
            for value in obj.values():
                o, a, p = self._count_elements(value)
                objects_count += o
                arrays_count += a
                primitives_count += p
        elif isinstance(obj, list):
            arrays_count += 1
            for item in obj:
                o, a, p = self._count_elements(item)
                objects_count += o
                arrays_count += a
                primitives_count += p
        else:
            primitives_count += 1

        return objects_count, arrays_count, primitives_count

    def _analyze_keys(
        self,
        obj: dict[str, Any] | list[Any] | object,
        key_stats: dict[str, int] | None = None,
    ) -> dict[str, int]:
        """Analyze key usage statistics."""
        if key_stats is None:
            key_stats = {}

        if isinstance(obj, dict):
            # Type narrowing: obj is now dict[str, Any]
            for key, value in obj.items():
                key_stats[key] = key_stats.get(key, 0) + 1  # type: ignore[index]
                self._analyze_keys(value, key_stats)
        elif isinstance(obj, list):
            for item in obj:
                self._analyze_keys(item, key_stats)

        return key_stats


class JsonProcessor:
    """Processor for JSON data transformation and manipulation."""

    @staticmethod
    def flatten_json(data: dict[str, Any], separator: str = "_") -> dict[str, Any]:
        """Flatten nested JSON structure."""

        def _flatten(
            obj: dict[str, Any] | list[Any] | object,
            parent_key: str = "",
        ) -> dict[str, Any]:
            items = []
            if isinstance(obj, dict):
                for key, value in obj.items():
                    new_key = f"{parent_key}{separator}{key!s}" if parent_key else str(key)
                    if isinstance(value, (dict, list)):
                        items.extend(_flatten(value, new_key).items())
                    else:
                        items.append((new_key, value))
            elif isinstance(obj, list):
                for i, value in enumerate(obj):
                    new_key = f"{parent_key}{separator}{i}"
                    if isinstance(value, (dict, list)):
                        items.extend(_flatten(value, new_key).items())
                    else:
                        items.append((new_key, value))
            else:
                items.append((parent_key, obj))
            return dict(items)

        return _flatten(data)

    @staticmethod
    def filter_json(
        data: dict[str, Any],
        include_keys: list[str] | None = None,
        exclude_keys: list[str] | None = None,
    ) -> dict[str, Any]:
        """Filter JSON data by including/excluding specific keys."""
        if include_keys is None:
            include_keys = []
        if exclude_keys is None:
            exclude_keys = []

        def _filter(
            obj: dict[str, Any] | list[Any] | object,
        ) -> dict[str, Any] | list[Any] | object:
            if isinstance(obj, dict):
                result = {}
                for key, value in obj.items():
                    if (not include_keys or key in include_keys) and key not in exclude_keys:
                        result[key] = _filter(value)
                return result
            if isinstance(obj, list):
                return [_filter(item) for item in obj]
            return obj

        result = _filter(data)
        # Type narrowing: ensure result is dict
        if not isinstance(result, dict):
            msg = f"Unexpected filter result: {type(result)}"
            raise TypeError(msg)
        return result  # type: ignore[return-value]

    @staticmethod
    def validate_json_schema(
        data: dict[str, Any],
        schema: dict[str, Any],
    ) -> tuple[bool, list[str]]:
        """Validate JSON data against a schema."""
        errors = []

        def _validate(
            obj: dict[str, Any] | list[Any] | object,
            schema_def: dict[str, Any],
            path: str = "",
        ) -> None:
            if isinstance(schema_def, dict):
                if not isinstance(obj, dict):
                    errors.append(f"{path}: Expected object, got {type(obj).__name__}")
                    return

                required_fields = schema_def.get("required", [])
                errors.extend(
                    f"{path}.{field}: Required field missing" for field in required_fields if field not in obj
                )

                properties = schema_def.get("properties", {})
                for key, value in obj.items():
                    if key in properties:
                        _validate(
                            value,
                            properties[key],
                            f"{path}.{key}" if path else str(key),
                        )

            elif isinstance(schema_def, list) and schema_def:
                if not isinstance(obj, list):
                    errors.append(f"{path}: Expected array, got {type(obj).__name__}")
                    return

                item_schema = schema_def[0]
                for i, item in enumerate(obj):
                    _validate(item, item_schema, f"{path}[{i}]")

            # Type checking
            expected_type = schema_def.get("type") if isinstance(schema_def, dict) else None
            if expected_type:
                type_mapping = {
                    "string": str,
                    "number": (int, float),
                    "integer": int,
                    "boolean": bool,
                    "array": list,
                    "object": dict,
                }

                if expected_type in type_mapping:
                    expected_python_type = type_mapping[expected_type]
                    if not isinstance(obj, expected_python_type):
                        errors.append(
                            f"{path}: Expected {expected_type}, got {type(obj).__name__}",
                        )

        _validate(data, schema)
        return len(errors) == 0, errors
