"""Data client for INE Portugal API."""

import logging
from collections.abc import Iterator
from pathlib import Path
from typing import Any, Optional, Union, cast

from pyptine.client.base import INEClient
from pyptine.client.metadata import MetadataClient  # Import MetadataClient
from pyptine.models.response import DataResponse
from pyptine.utils.exceptions import DataProcessingError, DimensionError

logger = logging.getLogger(__name__)


class DataClient(INEClient):
    """Client for INE data API endpoint.

    Fetches and parses indicator data with support for dimension filtering
    and pagination for large datasets.

    Example:
        >>> client = DataClient(language="EN")
        >>> response = client.get_data("0004167")
        >>> df = response.to_dataframe()
    """

    DATA_ENDPOINT = "/ine/json_indicador/pindica.jsp"
    DEFAULT_PAGE_SIZE = 40000  # API limit for data points per request

    def __init__(
        self,
        language: str = "EN",
        timeout: int = 30,
        cache_enabled: bool = True,
        cache_dir: Optional[Path] = None,
        metadata_client: Optional[MetadataClient] = None,  # New parameter
    ):
        super().__init__(language, timeout, cache_enabled, cache_dir)
        self.metadata_client = metadata_client

    def get_data(
        self,
        varcd: str,
        dimensions: Optional[dict[str, str]] = None,
    ) -> DataResponse:
        """Fetch indicator data with optional dimension filters.

        Args:
            varcd: Indicator code (e.g., "0004167")
            dimensions: Optional dimension filters (e.g., {"Dim1": "2023", "Dim2": "1"})

        Returns:
            DataResponse object with indicator data

        Raises:
            DimensionError: If dimension filters are invalid
            DataProcessingError: If response parsing fails

        Example:
            >>> client = DataClient()
            >>> # Get all data
            >>> response = client.get_data("0004167")
            >>> # Get filtered data
            >>> response = client.get_data(
            ...     "0004167",
            ...     dimensions={"Dim1": "2023", "Dim2": "1"}
            ... )
            >>> df = response.to_dataframe()
        """
        logger.info(f"Fetching data for indicator {varcd}")

        params = self._build_params(varcd, dimensions)

        try:
            raw_response = self._make_request(
                self.DATA_ENDPOINT, params=params, response_format="json"
            )

            # Parse response
            data_response = self._parse_data_response(
                varcd, cast(Union[dict[str, Any], list[dict[str, Any]]], raw_response)
            )

            logger.info(f"Retrieved {len(data_response.data)} data points for {varcd}")

            return data_response

        except Exception as e:
            logger.error(f"Failed to get data for {varcd}: {str(e)}")
            raise

    def get_all_data(
        self,
        varcd: str,
        dimensions: Optional[dict[str, str]] = None,
        chunk_size: int = DEFAULT_PAGE_SIZE,
    ) -> Iterator[DataResponse]:
        """Fetch all data for a given indicator with pagination support.

        Implements chunked data fetching to handle large datasets efficiently.
        The INE API returns data in chunks; this method automatically handles
        pagination by fetching subsequent chunks until all data is retrieved.

        Args:
            varcd: Indicator code
            dimensions: Optional dimension filters
            chunk_size: Number of data points per chunk (default: 40,000)

        Yields:
            DataResponse objects, one per chunk. Each chunk contains up to
            chunk_size data points.

        Example:
            >>> client = DataClient()
            >>> for chunk in client.get_all_data("0004167"):
            ...     df = chunk.to_dataframe()
            ...     print(f"Processed {len(df)} rows")
            >>> # Or collect all chunks
            >>> all_chunks = list(client.get_all_data("0004167"))
            >>> total_points = sum(len(chunk.data) for chunk in all_chunks)
        """
        logger.info(f"Fetching all data for indicator {varcd} with chunk_size={chunk_size}")

        offset = 0
        total_fetched = 0
        chunk_count = 0

        while True:
            chunk_count += 1
            logger.debug(f"Fetching chunk {chunk_count} with offset={offset}")

            params = self._build_params(varcd, dimensions, offset=offset, limit=chunk_size)

            try:
                raw_response = self._make_request(
                    self.DATA_ENDPOINT, params=params, response_format="json"
                )

                # Parse response
                data_response = self._parse_data_response(
                    varcd, cast(Union[dict[str, Any], list[dict[str, Any]]], raw_response)
                )

                chunk_size_received = len(data_response.data)
                total_fetched += chunk_size_received

                logger.info(
                    f"Chunk {chunk_count}: Retrieved {chunk_size_received} data points "
                    f"(total so far: {total_fetched})"
                )

                yield data_response

                # If we received fewer data points than requested, we've reached the end
                if chunk_size_received < chunk_size:
                    logger.info(f"Completed fetch for {varcd}: total {total_fetched} data points")
                    break

                offset += chunk_size

            except Exception as e:
                logger.error(f"Failed to fetch chunk {chunk_count} for {varcd}: {str(e)}")
                raise

    def _build_params(
        self,
        varcd: str,
        dimensions: Optional[dict[str, str]] = None,
        offset: Optional[int] = None,
        limit: Optional[int] = None,
    ) -> dict[str, str]:
        """Build query parameters for data API request.

        Args:
            varcd: Indicator code
            dimensions: Optional dimension filters
            offset: Starting offset for pagination (0-based)
            limit: Maximum number of records to return

        Returns:
            Dictionary of query parameters

        Raises:
            DimensionError: If dimension keys or values are invalid

        Example:
            >>> params = client._build_params("0004167")
            >>> params = client._build_params("0004167", dimensions={"Dim1": "2023"})
            >>> params = client._build_params("0004167", offset=0, limit=1000)
        """
        params = {
            "op": "2",  # Operation code for data retrieval
            "varcd": varcd,
        }

        # Add pagination parameters if provided
        if offset is not None:
            params["start"] = str(offset)
        if limit is not None:
            params["count"] = str(limit)

        # Validate dimensions before building params
        if dimensions:
            self.validate_dimensions(varcd, dimensions)
            for key, value in dimensions.items():
                params[key] = str(value)

        return params

    def _parse_data_response(
        self, varcd: str, response: Union[dict[str, Any], list[dict[str, Any]]]
    ) -> DataResponse:
        """Parse data API response into DataResponse model.

        Args:
            varcd: Indicator code (used if response is a list)
            response: Raw JSON response from API (can be dict or list)

        Returns:
            Parsed DataResponse object

        Raises:
            DataProcessingError: If parsing fails
        """
        try:
            varcd_val = varcd
            title = ""
            language = self.language
            unit = None
            data_array = []

            if isinstance(response, list):
                # New API format: response is a list with a single dict
                if len(response) == 1 and isinstance(response[0], dict):
                    response = response[0]
                elif len(response) > 1:
                    # Old format: response is directly the data array
                    data_array = response
                    # Fetch metadata separately to get title and unit
                    if self.metadata_client:
                        try:
                            metadata = self.metadata_client.get_metadata(varcd)
                            title = metadata.title
                            unit = metadata.unit
                        except Exception as e:
                            logger.warning(
                                f"Could not fetch metadata for {varcd} when parsing list data response: {e}"
                            )
                    else:
                        logger.warning(
                            "MetadataClient not available in DataClient to fetch indicator name and unit."
                        )

                    if not title and data_array:
                        # Fallback: try to get unit from first data point if metadata not available
                        first_point = data_array[0]
                        unit = first_point.get("unidade") or first_point.get("unit")

            if isinstance(response, dict):
                # Support both old and new API formats
                # New format uses PascalCase field names
                varcd_val = response.get("IndicadorCod") or response.get("indicador", varcd)
                title = (
                    response.get("IndicadorDsg")
                    or response.get("IndicadorNome")
                    or response.get("nome", "")
                )
                language = response.get("Lingua") or response.get("lang", self.language)
                unit = response.get("UnidadeMedida") or response.get("unidade")

                # Handle both old and new data array formats
                # Old format: "dados" is a flat array
                # New format: "Dados" is an object with years as keys
                dados = response.get("Dados") or response.get("dados")
                if isinstance(dados, dict):
                    # New format: flatten all years into a single array
                    data_array = []
                    for year_data in dados.values():
                        if isinstance(year_data, list):
                            data_array.extend(year_data)
                elif isinstance(dados, list):
                    # Old format: already a flat array
                    data_array = dados
                else:
                    data_array = []

            # Process data points
            processed_data = []
            for data_point in data_array:
                processed_point = self._process_data_point(data_point)
                if processed_point:
                    processed_data.append(processed_point)

            # If unit is still None and we have metadata_client, fetch from metadata
            if unit is None and self.metadata_client:
                try:
                    metadata = self.metadata_client.get_metadata(varcd)
                    unit = metadata.unit
                    # Also update title if it's empty
                    if not title:
                        title = metadata.title
                except Exception as e:
                    logger.debug(f"Could not fetch unit from metadata for {varcd}: {e}")

            return DataResponse(
                varcd=varcd_val,
                title=title,
                language=language,
                data=processed_data,
                unit=unit,
            )

        except Exception as e:
            logger.error(f"Failed to parse data response: {str(e)}")
            raise DataProcessingError(f"Failed to parse data: {str(e)}") from e

    def _process_data_point(self, data_point: dict[str, Any]) -> Optional[dict[str, Any]]:
        """Process a single data point from the API response.

        Args:
            data_point: Raw data point dictionary

        Returns:
            Processed data point dictionary or None if invalid
        """
        try:
            processed = {}

            # Iterate through all fields in the data point
            for key, value in data_point.items():
                # Skip internal fields
                if key.startswith("_"):
                    continue

                # Handle value field specially (convert to float)
                if key == "valor" or key == "value":
                    try:
                        processed["value"] = float(value) if value is not None else None
                    except (ValueError, TypeError) as e:
                        logger.warning(
                            f"Could not convert value '{value}' for key '{key}' to float in data point {data_point}. "
                            f"Setting to None. Error: {e}"
                        )
                        processed["value"] = None
                else:
                    # Keep other fields as-is
                    processed[key] = value

            return processed

        except Exception as e:
            logger.warning(f"Failed to process data point {data_point}: {str(e)}")
            return None

    def validate_dimensions(self, varcd: str, dimensions: dict[str, str]) -> bool:
        """Validate dimension filters against indicator metadata.

        This method checks if the provided dimension codes and values are valid
        for the indicator.

        Args:
            varcd: Indicator code
            dimensions: Dimension filters to validate (e.g., {"Dim1": "2023"})

        Returns:
            True if dimensions are valid

        Raises:
            DimensionError: If metadata client is not available, or if any
                            dimension key or value is invalid.
        """
        if not self.metadata_client:
            raise DimensionError("MetadataClient not available for dimension validation.")

        metadata = self.metadata_client.get_metadata(varcd)
        available_dimensions = {f"Dim{d.id}": d for d in metadata.dimensions}

        for dim_key, dim_value in dimensions.items():
            if dim_key not in available_dimensions:
                raise DimensionError(
                    f"Invalid dimension key '{dim_key}' for indicator {varcd}. "
                    f"Available keys: {list(available_dimensions.keys())}"
                )

            dimension = available_dimensions[dim_key]
            valid_values = {val.code for val in dimension.values}

            if dim_value not in valid_values:
                raise DimensionError(
                    f"Invalid value '{dim_value}' for dimension '{dim_key}' "
                    f"of indicator {varcd}. Available values: {list(valid_values)}"
                )

        return True
