"""Main module for the artip library."""

from datetime import date, datetime, timedelta
from typing import Union, List, Dict, Any
import polars as pl
from .exceptions import InvalidIntervalError


class Artip:
    """A class for managing temporal intervals in Power BI.

    This class provides methods to generate scalar period IDs from time intervals,
    create calendar tables, and generate DAX UDFs for Power BI.

    Attributes:
        min_time: The minimum time boundary.
        max_time: The maximum time boundary.
        granularity: The time granularity ('year', 'month', 'day', 'hour', 'minute', 'second').
        period_max_size: Maximum size of a period in granularity units.
    """

    def __init__(
        self,
        min_time: Union[date, datetime],
        max_time: Union[date, datetime],
        granularity: str = 'day',
        period_max_size: Union[int, None] = None
    ) -> None:
        """Initialize the Artip instance.

        Args:
            min_time: Minimum time boundary.
            max_time: Maximum time boundary.
            granularity: Time granularity.
            period_max_size: Maximum period size in granularity units.

        Raises:
            ValueError: If parameters are invalid.
        """
        if min_time >= max_time:
            raise ValueError("min_time must be less than max_time")

        valid_granularities = ['year', 'month', 'day', 'hour', 'minute', 'second']
        if granularity not in valid_granularities:
            raise ValueError(f"granularity must be one of {valid_granularities}")

        self.min_time = min_time
        self.max_time = max_time
        self.granularity = granularity
        self.period_max_size = period_max_size

        self._min_id = 1
        self._max_id = self._calculate_time_id(max_time)
        self._time_id_size = len(str(self._max_id))

        range_span = self._max_id - self._min_id
        if range_span < 0:
            raise ValueError("Invalid internal range for time IDs")

        if period_max_size is None:
            self._period_max_size = range_span
        else:
            if period_max_size < 1:
                raise ValueError("period_max_size must be a positive integer")
            self._period_max_size = period_max_size

        self._period_size_digits = len(str(self._period_max_size))
        self._periods: List[Dict[str, Any]] = []

    def _normalize_to_datetime(self, value: Union[date, datetime]) -> datetime:
        """Return datetime for date or datetime values."""
        if isinstance(value, datetime):
            return value
        return datetime(value.year, value.month, value.day)

    def _time_difference(self, start: Union[date, datetime], end: Union[date, datetime]) -> int:
        """Compute difference in granularity units between two times."""
        start_dt = self._normalize_to_datetime(start)
        end_dt = self._normalize_to_datetime(end)

        if self.granularity == 'year':
            return end_dt.year - start_dt.year
        if self.granularity == 'month':
            return (end_dt.year - start_dt.year) * 12 + (end_dt.month - start_dt.month)
        if self.granularity == 'day':
            return (end_dt.date() - start_dt.date()).days
        if self.granularity == 'hour':
            return int((end_dt - start_dt).total_seconds() // 3600)
        if self.granularity == 'minute':
            return int((end_dt - start_dt).total_seconds() // 60)
        if self.granularity == 'second':
            return int((end_dt - start_dt).total_seconds())

        raise ValueError(f"Unsupported granularity: {self.granularity}")

    def _calculate_time_id(self, time: Union[date, datetime]) -> int:
        """Calculate time ID from a given time."""
        time_dt = self._normalize_to_datetime(time)
        min_dt = self._normalize_to_datetime(self.min_time)
        max_dt = self._normalize_to_datetime(self.max_time)

        if time_dt < min_dt:
            return 0

        if time_dt > max_dt:
            return int('9' * self._time_id_size) if hasattr(self, '_time_id_size') else 0

        offset = self._time_difference(min_dt, time_dt)
        return offset + 1

    def _add_granularity(self, start: Union[date, datetime], amount: int) -> Union[date, datetime]:
        """Return a new datetime/date by adding amount units of granularity."""
        if self.granularity == 'year':
            return date(start.year + amount, start.month, start.day) if isinstance(start, date) and not isinstance(start, datetime) else datetime(start.year + amount, start.month, start.day, start.hour, start.minute, start.second)

        if self.granularity == 'month':
            total_months = start.year * 12 + (start.month - 1) + amount
            new_year = total_months // 12
            new_month = total_months % 12 + 1
            day = min(start.day, [31, 29 if (new_year % 4 == 0 and new_year % 100 != 0) or (new_year % 400 == 0) else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][new_month - 1])
            if isinstance(start, datetime):
                return datetime(new_year, new_month, day, start.hour, start.minute, start.second)
            return date(new_year, new_month, day)

        if self.granularity == 'day':
            delta = timedelta(days=amount)
        elif self.granularity == 'hour':
            delta = timedelta(hours=amount)
        elif self.granularity == 'minute':
            delta = timedelta(minutes=amount)
        elif self.granularity == 'second':
            delta = timedelta(seconds=amount)
        else:
            raise ValueError(f"Unsupported granularity: {self.granularity}")

        if isinstance(start, datetime):
            return start + delta
        return (datetime(start.year, start.month, start.day) + delta).date()

    def make_period_id(self, start: Union[date, datetime], end: Union[date, datetime]) -> int:
        """Generate a period ID for the given start and end times.

        Args:
            start: Start time of the period.
            end: End time of the period.

        Returns:
            The period ID as an integer.

        Raises:
            InvalidIntervalError: If the period is invalid.
        """
        if start > end:
            raise InvalidIntervalError("Start must be less or equal to end")

        start_id = self._calculate_time_id(start)
        end_id = self._calculate_time_id(end)

        length = end_id - start_id
        if length < 0:
            raise InvalidIntervalError("End time before start time after normalization")

        if length > self._period_max_size:
            raise InvalidIntervalError("Period size exceeds maximum allowed")

        period_id = start_id * (10 ** self._period_size_digits) + length

        self._periods.append({
            'start': start,
            'end': end,
            'period_id': period_id,
            'start_id': start_id,
            'end_id': end_id,
            'length': length
        })

        return period_id

    def _date_iterator(self, start: Union[date, datetime], end: Union[date, datetime]):
        """Yield each boundary value from start to end inclusive based on granularity."""
        current = start
        while current <= end:
            yield current
            current = self._add_granularity(current, 1)

    def make_calendar_table(self) -> pl.LazyFrame:
        """Generate a calendar table for the generated periods.

        Returns:
            A Polars LazyFrame representing the calendar.
        """
        records = []

        for period in self._periods:
            start = period['start']
            end = period['end']
            pid = period['period_id']

            for entry in self._date_iterator(start, end):
                records.append({'date': entry, 'period_id': pid, 'label': 'dans'})

            records.append({'date': start, 'period_id': pid, 'label': 'débute'})
            records.append({'date': end, 'period_id': pid, 'label': 'termine'})

        return pl.from_dicts(records).lazy()

    def get_DAX_UDF(self) -> str:
        """Generate DAX UDF code for Power BI.

        Returns:
            The DAX UDF as a string.
        """
        min_date = self.min_time if isinstance(self.min_time, date) else self.min_time.date()
        power = 10 ** self._period_size_digits

        date_from_id = {
            'year': "DATE(YEAR(minDate) + startId - 1, 1, 1)",
            'month': "EDATE(DATE(YEAR(minDate), MONTH(minDate), 1), startId - 1)",
            'day': "minDate + startId - 1",
            'hour': "minDate + (startId - 1) / 24",
            'minute': "minDate + (startId - 1) / 1440",
            'second': "minDate + (startId - 1) / 86400",
        }

        base = f"VAR minDate = DATE({min_date.year}, {min_date.month}, {min_date.day})\n"
        base += "VAR periodId = [PeriodId]\n"
        base += f"VAR startId = INT(periodId / {power})\n"
        base += f"VAR duration = periodId - startId * {power}\n"
        base += "VAR endId = startId + duration\n"
        base += f"VAR startDate = {date_from_id[self.granularity]}\n"
        base += f"VAR endDate = {date_from_id[self.granularity].replace('startId', 'endId')}\n"
        base += "RETURN FORMAT(startDate, \"yyyy-MM-dd\") & \" - \" & FORMAT(endDate, \"yyyy-MM-dd\")"

        return "// DAX UDF generated by Artip\n" + "Artip_PeriodToText =\n" + base
