"""TimescaleDB storage backend for django-devicehub.

Extends the Django ORM backend with TimescaleDB-specific optimizations:
  - Hypertable creation via a custom migration operation
  - time_bucket() for efficient time-series aggregation
  - Continuous aggregates (materialized views refreshed automatically)
  - Retention policies for automatic data expiration

Requirements:
    pip install django-devicehub[timescale]

    # or manually:
    pip install psycopg2-binary>=2.9   # or psycopg[binary]>=3.1

Setup:
    1. Install TimescaleDB extension on your PostgreSQL server
       (see https://docs.timescale.com/install/)

    2. Enable the extension in your database::

        CREATE EXTENSION IF NOT EXISTS timescaledb;

    3. Configure django-devicehub to use this backend::

        DJANGO_DEVICEHUB = {
            "STORAGE": {
                "ENGINE": "django_devicehub.storage.timescale.TimescaleDBStorage",
            },
        }

    4. In a Django migration, convert your reading tables to hypertables::

        from django_devicehub.storage.timescale import CreateHypertable

        class Migration(migrations.Migration):
            dependencies = [("myapp", "0001_initial")]
            operations = [
                CreateHypertable("myapp_weatherstationreading", "timestamp"),
            ]

    Optionally add retention and continuous aggregates via management
    commands or manual SQL after the hypertable is created.
"""

import logging
from datetime import timedelta

from django.db import connection, migrations

from .django_orm import AGGREGATE_FUNCTIONS, DjangoORMStorage

logger = logging.getLogger("django_devicehub.storage.timescale")


# ---------------------------------------------------------------------------
# Migration operation
# ---------------------------------------------------------------------------


class CreateHypertable(migrations.operations.base.Operation):
    """Django migration operation that converts a table to a TimescaleDB hypertable.

    Usage in a migration::

        from django_devicehub.storage.timescale import CreateHypertable

        class Migration(migrations.Migration):
            operations = [
                CreateHypertable(
                    table_name="myapp_weatherstationreading",
                    time_column="timestamp",
                    chunk_time_interval="7 days",
                    if_not_exists=True,
                ),
            ]

    Parameters:
        table_name: The database table name to convert.
        time_column: The column used as the time dimension (default: "timestamp").
        chunk_time_interval: Size of each hypertable chunk (default: "7 days").
        if_not_exists: Skip if already a hypertable (default: True).
    """

    reversible = True
    reduces_to_sql = True

    def __init__(
        self,
        table_name,
        time_column="timestamp",
        chunk_time_interval="7 days",
        if_not_exists=True,
    ):
        self.table_name = table_name
        self.time_column = time_column
        self.chunk_time_interval = chunk_time_interval
        self.if_not_exists = if_not_exists

    def state_forwards(self, app_label, state):
        # No model state change — this is a DB-level operation.
        pass

    def database_forwards(self, app_label, schema_editor, from_state, to_state):
        if_not_exists_sql = "true" if self.if_not_exists else "false"
        sql = (
            f"SELECT create_hypertable("
            f"'{self.table_name}', '{self.time_column}', "
            f"chunk_time_interval => INTERVAL '{self.chunk_time_interval}', "
            f"if_not_exists => {if_not_exists_sql}"
            f");"
        )
        schema_editor.execute(sql)

    def database_backwards(self, app_label, schema_editor, from_state, to_state):
        # TimescaleDB does not support reverting a hypertable to a regular table.
        # The reverse migration is a no-op; dropping the table is handled by
        # Django's normal DeleteModel operation.
        logger.info(
            "CreateHypertable reverse is a no-op for '%s'. "
            "Drop the table to fully remove the hypertable.",
            self.table_name,
        )

    def describe(self):
        return f"Create TimescaleDB hypertable on {self.table_name}"

    def deconstruct(self):
        return (
            self.__class__.__qualname__,
            [],
            {
                "table_name": self.table_name,
                "time_column": self.time_column,
                "chunk_time_interval": self.chunk_time_interval,
                "if_not_exists": self.if_not_exists,
            },
        )


# ---------------------------------------------------------------------------
# Storage backend
# ---------------------------------------------------------------------------


class TimescaleDBStorage(DjangoORMStorage):
    """TimescaleDB-optimized storage backend.

    Inherits all ORM methods from DjangoORMStorage and overrides
    aggregate() to use TimescaleDB's ``time_bucket()`` function, which
    is significantly faster than Django's ``Trunc()`` on hypertables.

    Also provides helpers for continuous aggregates and retention policies.
    """

    def aggregate(
        self,
        reading_model,
        field,
        function="avg",
        device=None,
        start=None,
        end=None,
        interval=None,
    ):
        """Aggregate readings using TimescaleDB time_bucket() when an interval
        is specified, falling back to the ORM for simple aggregation.

        Parameters:
            reading_model: The Django model class for readings.
            field: The column name to aggregate (e.g., "temperature").
            function: Aggregation function — "avg", "min", "max", "sum", "count".
            device: Optional device instance to filter by.
            start: Optional start datetime.
            end: Optional end datetime.
            interval: Time bucket size as a string ("5 minutes", "1 hour", "1 day")
                      or a ``timedelta``. If None, returns a scalar aggregate.

        Returns:
            Without interval: dict with ``{"result": <value>}``
            With interval: list of dicts ``[{"bucket": datetime, "result": value}, ...]``
        """
        agg_func = AGGREGATE_FUNCTIONS.get(function)
        if agg_func is None:
            raise ValueError(f"Unknown aggregate function: {function}")

        # Simple (non-bucketed) aggregation — use ORM
        if interval is None:
            return super().aggregate(
                reading_model, field, function, device, start, end, interval=None
            )

        # Bucketed aggregation — use time_bucket() via raw SQL
        interval_str = self._normalize_interval(interval)
        table = reading_model._meta.db_table
        sql_func = function.upper()

        where_clauses = []
        params = []

        if device is not None:
            where_clauses.append("device_id = %s")
            params.append(device.pk)
        if start is not None:
            where_clauses.append("timestamp >= %s")
            params.append(start)
        if end is not None:
            where_clauses.append("timestamp <= %s")
            params.append(end)

        where_sql = ""
        if where_clauses:
            where_sql = "WHERE " + " AND ".join(where_clauses)

        sql = (
            f"SELECT time_bucket(%s, timestamp) AS bucket, "
            f"{sql_func}({field}) AS result "
            f"FROM {table} "
            f"{where_sql} "
            f"GROUP BY bucket "
            f"ORDER BY bucket"
        )
        params.insert(0, interval_str)

        with connection.cursor() as cursor:
            cursor.execute(sql, params)
            columns = [col[0] for col in cursor.description]
            return [dict(zip(columns, row)) for row in cursor.fetchall()]

    def continuous_aggregate(
        self,
        name,
        reading_model,
        field,
        function="avg",
        interval="1 hour",
        start_offset="1 month",
        end_offset="1 hour",
        schedule_interval="1 hour",
        with_data=True,
    ):
        """Create a TimescaleDB continuous aggregate (materialized view).

        Continuous aggregates are automatically refreshed by TimescaleDB,
        giving you pre-computed rollups with minimal query latency.

        Parameters:
            name: Name for the materialized view (e.g., "hourly_temperature").
            reading_model: The reading model whose table is the hypertable.
            field: Column to aggregate.
            function: Aggregation function (avg, min, max, sum, count).
            interval: Bucket size (e.g., "1 hour", "1 day").
            start_offset: How far back the refresh policy looks (e.g., "1 month").
            end_offset: Freshness lag — how recent data can be before refresh
                        (e.g., "1 hour").
            schedule_interval: How often the refresh job runs (e.g., "1 hour").
            with_data: If True, backfill existing data immediately.

        Example::

            storage.continuous_aggregate(
                name="hourly_avg_temperature",
                reading_model=WeatherStationReading,
                field="temperature",
                function="avg",
                interval="1 hour",
            )
        """
        table = reading_model._meta.db_table
        sql_func = function.upper()
        with_data_sql = "WITH DATA" if with_data else "WITH NO DATA"

        create_sql = (
            f"CREATE MATERIALIZED VIEW IF NOT EXISTS {name} "
            f"WITH (timescaledb.continuous) AS "
            f"SELECT time_bucket('{interval}', timestamp) AS bucket, "
            f"device_id, "
            f"{sql_func}({field}) AS {function}_{field} "
            f"FROM {table} "
            f"GROUP BY bucket, device_id "
            f"{with_data_sql};"
        )

        policy_sql = (
            f"SELECT add_continuous_aggregate_policy('{name}', "
            f"start_offset => INTERVAL '{start_offset}', "
            f"end_offset => INTERVAL '{end_offset}', "
            f"schedule_interval => INTERVAL '{schedule_interval}', "
            f"if_not_exists => true);"
        )

        with connection.cursor() as cursor:
            cursor.execute(create_sql)
            cursor.execute(policy_sql)
            logger.info(
                "Created continuous aggregate '%s' on %s.%s (%s, bucket=%s)",
                name,
                table,
                field,
                function,
                interval,
            )

    def create_retention_policy(self, reading_model, drop_after="90 days"):
        """Add a TimescaleDB retention policy that automatically drops chunks
        older than ``drop_after``.

        Parameters:
            reading_model: The reading model whose hypertable gets the policy.
            drop_after: Age after which chunks are dropped.
                        String interval (e.g., "90 days") or a timedelta.

        Example::

            storage.create_retention_policy(WeatherStationReading, drop_after="30 days")
        """
        table = reading_model._meta.db_table

        if isinstance(drop_after, timedelta):
            total_seconds = int(drop_after.total_seconds())
            drop_after = f"{total_seconds} seconds"

        sql = (
            f"SELECT add_retention_policy('{table}', "
            f"INTERVAL '{drop_after}', if_not_exists => true);"
        )

        with connection.cursor() as cursor:
            cursor.execute(sql)
            logger.info(
                "Retention policy on '%s': drop chunks older than %s",
                table,
                drop_after,
            )

    def remove_retention_policy(self, reading_model):
        """Remove the retention policy from a hypertable.

        Parameters:
            reading_model: The reading model whose policy to remove.
        """
        table = reading_model._meta.db_table
        sql = f"SELECT remove_retention_policy('{table}', if_exists => true);"

        with connection.cursor() as cursor:
            cursor.execute(sql)
            logger.info("Removed retention policy from '%s'", table)

    def hypertable_info(self, reading_model):
        """Return information about a hypertable (chunk count, size, etc.).

        Returns:
            dict with hypertable metadata, or None if not a hypertable.
        """
        table = reading_model._meta.db_table

        sql = (
            "SELECT hypertable_name, num_chunks, "
            "pg_size_pretty(hypertable_size(%s)) AS total_size "
            "FROM timescaledb_information.hypertables "
            "WHERE hypertable_name = %s;"
        )

        with connection.cursor() as cursor:
            cursor.execute(sql, [table, table])
            row = cursor.fetchone()
            if row is None:
                return None
            columns = [col[0] for col in cursor.description]
            return dict(zip(columns, row))

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    @staticmethod
    def _normalize_interval(interval):
        """Convert an interval to a string suitable for time_bucket().

        Accepts:
            - str: passed through ("5 minutes", "1 hour", "1 day")
            - timedelta: converted to a PostgreSQL interval string
        """
        if isinstance(interval, str):
            return interval

        if isinstance(interval, timedelta):
            total_seconds = int(interval.total_seconds())
            if total_seconds < 60:
                return f"{total_seconds} seconds"
            elif total_seconds < 3600:
                minutes = total_seconds // 60
                return f"{minutes} minutes"
            elif total_seconds < 86400:
                hours = total_seconds // 3600
                return f"{hours} hours"
            else:
                days = total_seconds // 86400
                return f"{days} days"

        return "1 hour"
