"""
Base classes for NetOrca SDK v2.

Provides abstract base classes for resources and models with type hints.
"""

from __future__ import annotations

from abc import ABC
from datetime import datetime
from typing import TYPE_CHECKING, Any, Dict, Generator, List, Optional, Type

if TYPE_CHECKING:
    from netorca_sdk.v2.client import NetOrcaClient


class BaseModel(ABC):
    """
    Abstract base class for all data models.

    Provides common functionality for working with API data objects including
    property access, updates, and serialization.
    """

    def __init__(self, data: Dict[str, Any], client: Optional[NetOrcaClient] = None):
        """
        Initialize model with API response data.

        Args:
            data: Raw API response data
            client: Optional API client for instance operations (update, delete, refresh)
        """
        self.data = data
        self.client = client

    @property
    def id(self) -> Optional[int]:
        """Get resource ID."""
        return self.data.get('id')

    @property
    def url(self) -> Optional[str]:
        """Get resource URL."""
        return self.data.get('url')

    @property
    def created(self) -> Optional[datetime]:
        """Get creation timestamp."""
        created = self.data.get('created')
        if created:
            return self._parse_datetime(created)
        return None

    @property
    def modified(self) -> Optional[datetime]:
        """Get last modified timestamp."""
        modified = self.data.get('modified')
        if modified:
            return self._parse_datetime(modified)
        return None

    @staticmethod
    def _parse_datetime(dt_str: str) -> datetime:
        """Parse ISO 8601 datetime string."""
        # Handle both Z and +00:00 timezone formats
        if dt_str.endswith('Z'):
            dt_str = dt_str[:-1] + '+00:00'
        return datetime.fromisoformat(dt_str)

    @staticmethod
    def _extract_nested_id(obj: Any) -> Optional[int]:
        """
        Extract ID from nested object or return direct ID.

        Args:
            obj: Either a dict with 'id' key or an int

        Returns:
            ID if found, None otherwise
        """
        if isinstance(obj, dict):
            return obj.get('id')
        elif isinstance(obj, int):
            return obj
        return None

    @staticmethod
    def _extract_nested_field(obj: Any, field: str) -> Optional[str]:
        """
        Extract field from nested object.

        Args:
            obj: Dict containing the field
            field: Field name to extract

        Returns:
            Field value if found, None otherwise
        """
        if isinstance(obj, dict):
            return obj.get(field)
        return None

    def to_dict(self) -> Dict[str, Any]:
        """
        Convert model to dictionary.

        Returns:
            Raw API data dictionary
        """
        return self.data.copy()

    def refresh(self) -> BaseModel:
        """
        Refresh model data from API.

        Returns:
            New instance with updated data

        Raises:
            Exception: If client is not available
        """
        if not self.client:
            raise Exception("Cannot refresh: model has no client reference")

        # Subclasses should override if they need custom refresh logic
        raise NotImplementedError("Refresh not implemented for this model")

    def update(self, **kwargs: Any) -> BaseModel:
        """
        Update model via API.

        Args:
            **kwargs: Fields to update

        Returns:
            New instance with updated data

        Raises:
            Exception: If client is not available
        """
        if not self.client:
            raise Exception("Cannot update: model has no client reference")

        # Subclasses should override if they need custom update logic
        raise NotImplementedError("Update not implemented for this model")

    def delete(self) -> bool:
        """
        Delete model via API.

        Returns:
            True if successful

        Raises:
            Exception: If client is not available
        """
        if not self.client:
            raise Exception("Cannot delete: model has no client reference")

        # Subclasses should override if they need custom delete logic
        raise NotImplementedError("Delete not implemented for this model")

    def __repr__(self) -> str:
        """String representation of model."""
        return f"{self.__class__.__name__}(id={self.id})"


class BaseResource(ABC):
    """
    Abstract base class for all API resources.

    Provides common CRUD operations for working with API endpoints.
    Each resource should specify an endpoint and model_class.
    """

    endpoint: Optional[str] = None
    model_class: Optional[Type[BaseModel]] = None

    # Optional: Define supported filters for validation
    # Format: {"filter_name": type, ...}
    SUPPORTED_FILTERS: Optional[Dict[str, type]] = None

    def __init__(self, client: NetOrcaClient):
        """
        Initialize resource with API client.

        Args:
            client: API client instance
        """
        self.client = client

        if not self.endpoint:
            raise ValueError(f"{self.__class__.__name__} must define 'endpoint' attribute")
        if not self.model_class:
            raise ValueError(f"{self.__class__.__name__} must define 'model_class' attribute")

    def list(self, **filters: Any) -> Generator[BaseModel, None, None]:
        """
        List resources with optional filtering.

        Automatically handles pagination and yields model instances.

        Args:
            **filters: Query parameters for filtering

        Yields:
            Model instances
        """
        # Validate filters if SUPPORTED_FILTERS is defined
        if self.SUPPORTED_FILTERS:
            self._validate_filters(filters)

        # Use client's pagination generator
        for item_data in self.client.get(self.endpoint, params=filters):
            yield self._to_model(item_data)

    def get(self, id: int) -> BaseModel:
        """
        Get a single resource by ID.

        Args:
            id: Resource ID

        Returns:
            Model instance
        """
        data = self.client.get_one(self.endpoint, id)
        return self._to_model(data)

    def create(self, data: Dict[str, Any]) -> BaseModel:
        """
        Create a new resource.

        Args:
            data: Resource data

        Returns:
            Created model instance
        """
        response_data = self.client.post(self.endpoint, data)
        return self._to_model(response_data)

    def update(self, id: int, data: Dict[str, Any], partial: bool = True) -> BaseModel:
        """
        Update a resource.

        Args:
            id: Resource ID
            data: Updated fields
            partial: If True, use PATCH (default), else use PUT

        Returns:
            Updated model instance
        """
        if partial:
            response_data = self.client.patch(self.endpoint, id, data)
        else:
            response_data = self.client.put(self.endpoint, id, data)
        return self._to_model(response_data)

    def delete(self, id: int) -> bool:
        """
        Delete a resource.

        Args:
            id: Resource ID

        Returns:
            True if successful
        """
        self.client.delete(self.endpoint, id)
        return True

    def filter(self, **filters: Any) -> List[BaseModel]:
        """
        List resources and return as list instead of generator.

        Convenience method that loads all results into memory.

        Args:
            **filters: Query parameters for filtering

        Returns:
            List of model instances
        """
        return list(self.list(**filters))

    def _to_model(self, data: Dict[str, Any]) -> BaseModel:
        """
        Convert API response dict to model instance.

        Args:
            data: Raw API response data

        Returns:
            Model instance with client reference
        """
        return self.model_class(data=data, client=self.client)

    def _validate_filters(self, filters: Dict[str, Any]) -> None:
        """
        Validate filter parameters against SUPPORTED_FILTERS.

        Args:
            filters: Filter dict to validate

        Raises:
            ValueError: If unsupported filter is used
        """
        if not self.SUPPORTED_FILTERS:
            return

        for filter_name in filters.keys():
            if filter_name not in self.SUPPORTED_FILTERS:
                supported = ', '.join(self.SUPPORTED_FILTERS.keys())
                raise ValueError(
                    f"Unsupported filter '{filter_name}'. "
                    f"Supported filters: {supported}"
                )
