"""
CSV Report Helper - Generate CSV reports from Pydantic models

This helper provides a decorator-based approach to automatically generate
CSV reports from Pydantic models with minimal code changes.
"""

import csv
from pathlib import Path
from typing import Type, Any, Dict, Optional, TypeVar, List
from datetime import datetime
import threading
from pydantic import BaseModel
from logging import getLogger

from csv_report.csv_report_auto_calculate import CsvReportAutoCalculate

logger = getLogger(__name__)
T = TypeVar('T', bound=BaseModel)

# Global registry to store filename mappings
_csv_filename_registry: Dict[Type[BaseModel], str] = {}
_lock = threading.Lock()


def csv_report_file(filename: str):
    """
    Decorator to register a Pydantic model with a CSV filename.
    
    Args:
        filename: The CSV filename (without .csv extension)
        
    Usage:
        @csv_report_file("device_info")
        class DeviceInfoReport(BaseModel):
            timestamp: datetime
            device_id: str
            ram_buffer_start: int
            ram_buffer_size: int
    """
    def decorator(cls: Type[T]) -> Type[T]:
        assert issubclass(cls, BaseModel), (
            "csv_report_file decorator can only be used with Pydantic"
            " BaseModel classes")
        
        # Ensure filename has .csv extension
        csv_filename = filename if filename.endswith('.csv') else f"{filename}.csv"
        
        # Register the class with its filename
        _csv_filename_registry[cls] = csv_filename
        
        return cls
    
    return decorator


class CsvReportHelper:
    """Helper class for managing CSV reports from Pydantic models."""
    
    BASE_DIR = Path("logs/csv_report")
    
    @classmethod
    def _ensure_directory_exists(cls) -> None:
        """Ensure the CSV report directory exists."""
        cls.BASE_DIR.mkdir(parents=True, exist_ok=True)
    
    @classmethod
    def _get_csv_filepath(cls, model_class: Type[BaseModel],
                          subtitle: str = "") -> Path:
        """Get the full CSV file path for a Pydantic model class."""
        assert model_class in _csv_filename_registry, \
            f"Model class {model_class.__name__} is not registered with" \
            " @csv_report_file decorator"
        
        file_name = _csv_filename_registry[model_class]
        if subtitle != "":
            file_name = f"{file_name}_{subtitle}"
        return cls.BASE_DIR / file_name
    
    @classmethod
    def _get_csv_headers(cls, model_class: Type[BaseModel]) -> List[str]:
        """Get CSV headers from Pydantic model fields."""
        return list(model_class.model_fields.keys())
    
    @classmethod
    def _file_exists_and_has_content(cls, filepath: Path) -> bool:
        """Check if file exists and has content (at least headers)."""
        return filepath.exists() and filepath.stat().st_size > 0

    @classmethod
    def _read_existing_headers(cls, filepath: Path) -> Optional[List[str]]:
        """Read the existing headers from CSV file."""
        if not cls._file_exists_and_has_content(filepath):
            return None

        try:
            with open(filepath, 'r', newline='', encoding='utf-8') as csvfile:
                reader = csv.reader(csvfile)
                headers = next(reader, None)
                return headers if headers else None
        except Exception:
            # If we can't read the file, treat it as if it doesn't exist
            return None

    @classmethod
    def _headers_match(cls, existing_headers: List[str], expected_headers: List[str]) -> bool:
        """Check if existing headers match expected headers."""
        return existing_headers == expected_headers

    @classmethod
    def _create_backup_filename(cls, filepath: Path) -> Path:
        """Create a backup filename with timestamp."""
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        stem = filepath.stem
        suffix = filepath.suffix
        backup_name = f"{stem}_backup_{timestamp}{suffix}"
        return filepath.parent / backup_name
    
    @classmethod
    def _write_headers(cls, filepath: Path, headers: List[str]) -> None:
        """Write CSV headers to file."""
        with open(filepath, 'w', newline='', encoding='utf-8') as csvfile:
            writer = csv.writer(csvfile)
            writer.writerow(headers)
    
    @classmethod
    def _append_row(cls, filepath: Path, row_data: List[Any]) -> None:
        """Append a data row to CSV file."""
        with open(filepath, 'a', newline='', encoding='utf-8') as csvfile:
            writer = csv.writer(csvfile)
            writer.writerow(row_data)
    
    @classmethod
    def append(cls, pydantic_object: BaseModel, subtitle: str = "") -> None:
        """
        Append a Pydantic object to its corresponding CSV file.
        
        Args:
            pydantic_object: Instance of a Pydantic model decorated with @csv_report_file
        """
        assert isinstance(pydantic_object, BaseModel), \
            "Object must be an instance of Pydantic BaseModel"

        if isinstance(pydantic_object, CsvReportAutoCalculate):
            pydantic_object.update_before_save()

        model_class = type(pydantic_object)
        
        with _lock:  # Thread-safe file operations
            # Ensure directory exists
            cls._ensure_directory_exists()
            
            # Get file path
            filepath = cls._get_csv_filepath(model_class, subtitle)
            
            # Get expected headers from model
            expected_headers = cls._get_csv_headers(model_class)

            # Check if file needs headers or header validation
            needs_new_file = False

            if not cls._file_exists_and_has_content(filepath):
                # File doesn't exist or is empty
                needs_new_file = True
            else:
                # File exists, check if headers match
                existing_headers = cls._read_existing_headers(filepath)
                if existing_headers is None or not cls._headers_match(existing_headers, expected_headers):
                    # Headers don't match, need to create new file
                    needs_new_file = True

                    # Create backup of existing file if it has content
                    if existing_headers is not None:
                        backup_filepath = cls._create_backup_filename(filepath)
                        filepath.rename(backup_filepath)
                        logger.debug(f"[CSV Report] Header mismatch detected. Existing file backed up to: {backup_filepath}")
                        logger.debug(f"[CSV Report] Expected headers: {expected_headers}")
                        logger.debug(f"[CSV Report] Existing headers: {existing_headers}")

            if needs_new_file:
                cls._write_headers(filepath, expected_headers)
            
            # Convert Pydantic object to row data
            row_data = []
            for field_name in expected_headers:
                value = getattr(pydantic_object, field_name)
                # Handle datetime objects
                if isinstance(value, datetime):
                    value = value.isoformat()
                row_data.append(value)
            
            # Append row to file
            cls._append_row(filepath, row_data)
    
    @classmethod
    def get_registered_models(cls) -> Dict[Type[BaseModel], str]:
        """Get all registered model classes and their filenames."""
        return _csv_filename_registry.copy()
    
    @classmethod
    def clear_file(cls, model_class: Type[BaseModel]) -> None:
        """
        Clear the CSV file for a specific model class.
        
        Args:
            model_class: The Pydantic model class
        """
        filepath = cls._get_csv_filepath(model_class)
        if filepath.exists():
            filepath.unlink()
    
    @classmethod
    def read_csv_as_dicts(cls, model_class: Type[BaseModel]) -> List[Dict[str, Any]]:
        """
        Read CSV file and return as list of dictionaries.

        Args:
            model_class: The Pydantic model class

        Returns:
            List of dictionaries representing CSV rows
        """
        filepath = cls._get_csv_filepath(model_class)
        if not filepath.exists():
            return []

        with open(filepath, 'r', newline='', encoding='utf-8') as csvfile:
            reader = csv.DictReader(csvfile)
            return list(reader)

    @classmethod
    def validate_and_fix_headers(cls, model_class: Type[BaseModel], force_backup: bool = False) -> bool:
        """
        Validate CSV file headers and fix if necessary.

        Args:
            model_class: The Pydantic model class
            force_backup: If True, always create backup even if headers match

        Returns:
            True if headers were fixed/updated, False if they were already correct
        """
        filepath = cls._get_csv_filepath(model_class)

        if not cls._file_exists_and_has_content(filepath):
            print(f"[CSV Report] File {filepath} does not exist or is empty")
            return False

        expected_headers = cls._get_csv_headers(model_class)
        existing_headers = cls._read_existing_headers(filepath)

        if existing_headers is None:
            print(f"[CSV Report] Could not read headers from {filepath}")
            return False

        headers_match = cls._headers_match(existing_headers, expected_headers)

        if headers_match and not force_backup:
            print(f"[CSV Report] Headers already match for {model_class.__name__}")
            return False

        # Create backup and new file with correct headers
        backup_filepath = cls._create_backup_filename(filepath)
        filepath.rename(backup_filepath)

        print(f"[CSV Report] Headers {'updated' if not headers_match else 'backed up'}. Original file backed up to: {backup_filepath}")
        if not headers_match:
            print(f"[CSV Report] Expected headers: {expected_headers}")
            print(f"[CSV Report] Existing headers: {existing_headers}")

        # Create new file with correct headers
        cls._write_headers(filepath, expected_headers)

        return True
