"""InfluxDB 2.x storage backend for django-devicehub.

Stores time-series readings in InfluxDB while keeping device metadata
and messages in the Django ORM (PostgreSQL/SQLite).  This gives you
InfluxDB's purpose-built compression, retention, and Flux query engine
for the high-volume reading path.

Requirements:
    pip install django-devicehub[influxdb]

    # or manually:
    pip install influxdb-client>=1.30

Setup:
    1. Configure the backend and connection in settings::

        DJANGO_DEVICEHUB = {
            "STORAGE": {
                "ENGINE": "django_devicehub.storage.influxdb.InfluxDBStorage",
                "INFLUXDB_URL": "http://localhost:8086",
                "INFLUXDB_TOKEN": "my-token",
                "INFLUXDB_ORG": "my-org",
                "INFLUXDB_BUCKET": "iot_readings",
            },
        }

    2. Create the bucket in InfluxDB (via UI or CLI)::

        influx bucket create -n iot_readings -o my-org -r 90d

    3. Use the backend — store_reading() writes InfluxDB points, and
       query_readings()/aggregate() use Flux queries under the hood.

Notes:
    - Readings are stored as InfluxDB points where:
        measurement = device type name (lowercase)
        tags        = {device_id: "..."}
        fields      = {temperature: 25.3, humidity: 80.1, ...}
        time        = reading timestamp

    - The Django ORM reading model is NOT written to by this backend.
      If you need dual-write (ORM + InfluxDB), subclass and override.
"""

import logging
from datetime import datetime, timezone

from .base import BaseStorage

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

# Lazy import — only fail when actually used, not at import time.
_influxdb_client = None
_Point = None
_WritePrecision = None


def _ensure_influxdb():
    """Import influxdb-client on first use."""
    global _influxdb_client, _Point, _WritePrecision
    if _influxdb_client is not None:
        return

    try:
        import influxdb_client
        from influxdb_client.client.write_api import SYNCHRONOUS  # noqa: F401
        from influxdb_client.domain.write_precision import WritePrecision

        _influxdb_client = influxdb_client
        _Point = influxdb_client.client.write_api.Point
        _WritePrecision = WritePrecision
    except ImportError:
        raise ImportError(
            "influxdb-client is required for the InfluxDB storage backend.\n"
            "Install it with: pip install django-devicehub[influxdb]"
        )


class InfluxDBStorage(BaseStorage):
    """InfluxDB 2.x storage backend.

    Writes reading data as InfluxDB points and queries via Flux.
    Device metadata and raw messages remain in the Django ORM.
    """

    def __init__(self):
        _ensure_influxdb()

        from ..conf import devicehub_settings

        storage_conf = devicehub_settings.STORAGE
        self._url = storage_conf.get("INFLUXDB_URL", "http://localhost:8086")
        self._token = storage_conf.get("INFLUXDB_TOKEN", "")
        self._org = storage_conf.get("INFLUXDB_ORG", "")
        self._bucket = storage_conf.get("INFLUXDB_BUCKET", "iot_readings")

        self._client = _influxdb_client.InfluxDBClient(
            url=self._url,
            token=self._token,
            org=self._org,
        )
        self._write_api = self._client.write_api(
            write_options=_influxdb_client.client.write_api.SYNCHRONOUS
        )
        self._query_api = self._client.query_api()

    def close(self):
        """Close the InfluxDB client connection."""
        if self._client:
            self._client.close()

    # ------------------------------------------------------------------
    # BaseStorage interface
    # ------------------------------------------------------------------

    def store_reading(self, reading_model, device, timestamp, data):
        """Write a reading as an InfluxDB point.

        Parameters:
            reading_model: The Django reading model class (used to derive
                           the measurement name from the device type).
            device: The device instance.
            timestamp: A datetime for the data point.
            data: Dict of field_name -> value (e.g., {"temperature": 25.3}).

        Returns:
            True on success.
        """
        measurement = self._measurement_name(reading_model)

        point = _Point(measurement)
        point.tag("device_id", str(device.device_id))

        for field_name, value in data.items():
            if value is not None:
                point.field(field_name, self._coerce_field_value(value))

        # Ensure timezone-aware UTC
        if timestamp.tzinfo is None:
            timestamp = timestamp.replace(tzinfo=timezone.utc)
        point.time(timestamp, _WritePrecision.NS)

        try:
            self._write_api.write(bucket=self._bucket, record=point)
            return True
        except Exception:
            logger.exception(
                "Failed to write point to InfluxDB (device=%s, measurement=%s)",
                device.device_id,
                measurement,
            )
            raise

    def query_readings(
        self,
        reading_model,
        device=None,
        start=None,
        end=None,
        fields=None,
        limit=None,
        order="-timestamp",
    ):
        """Query readings from InfluxDB using Flux.

        Returns a list of dicts (not a Django QuerySet) with keys:
        ``device_id``, ``timestamp``, and one key per requested field.
        """
        measurement = self._measurement_name(reading_model)
        flux = self._build_flux_query(
            measurement,
            device=device,
            start=start,
            end=end,
            fields=fields,
            limit=limit,
            order=order,
        )

        try:
            tables = self._query_api.query(flux)
        except Exception:
            logger.exception("InfluxDB query failed: %s", flux)
            raise

        results = []
        for table in tables:
            for record in table.records:
                row = {
                    "device_id": record.values.get("device_id", ""),
                    "timestamp": record.get_time(),
                    record.get_field(): record.get_value(),
                }
                results.append(row)

        # Merge rows that share the same timestamp + device_id
        return self._merge_rows(results)

    def aggregate(
        self,
        reading_model,
        field,
        function="avg",
        device=None,
        start=None,
        end=None,
        interval=None,
    ):
        """Aggregate readings using InfluxDB native functions.

        Without ``interval``: returns ``{"result": <value>}``.
        With ``interval``: returns ``[{"bucket": datetime, "result": value}, ...]``.
        """
        measurement = self._measurement_name(reading_model)
        flux_fn = self._flux_aggregate_function(function)

        flux_parts = [
            f'from(bucket: "{self._bucket}")',
            f"  |> range({self._flux_range(start, end)})",
            f'  |> filter(fn: (r) => r._measurement == "{measurement}")',
            f'  |> filter(fn: (r) => r._field == "{field}")',
        ]

        if device is not None:
            flux_parts.append(
                f'  |> filter(fn: (r) => r.device_id == "{device.device_id}")'
            )

        if interval is None:
            # Scalar aggregate
            flux_parts.append(f"  |> {flux_fn}()")
            flux = "\n".join(flux_parts)

            try:
                tables = self._query_api.query(flux)
            except Exception:
                logger.exception("InfluxDB aggregate query failed")
                raise

            for table in tables:
                for record in table.records:
                    return {"result": record.get_value()}
            return {"result": None}
        else:
            # Time-bucketed aggregate
            window = self._normalize_flux_interval(interval)
            flux_parts.append(
                f"  |> aggregateWindow(every: {window}, fn: {flux_fn}, createEmpty: false)"
            )
            flux = "\n".join(flux_parts)

            try:
                tables = self._query_api.query(flux)
            except Exception:
                logger.exception("InfluxDB bucketed aggregate query failed")
                raise

            results = []
            for table in tables:
                for record in table.records:
                    results.append(
                        {
                            "bucket": record.get_time(),
                            "result": record.get_value(),
                        }
                    )
            return results

    # ------------------------------------------------------------------
    # InfluxDB-specific public methods
    # ------------------------------------------------------------------

    def delete_readings(self, reading_model, device=None, start=None, end=None):
        """Delete readings from InfluxDB.

        Parameters:
            reading_model: The reading model (for measurement name).
            device: Optional device to scope deletion.
            start: Start of deletion range (required by InfluxDB).
            end: End of deletion range (required by InfluxDB).
        """
        measurement = self._measurement_name(reading_model)
        delete_api = self._client.delete_api()

        predicate = f'_measurement="{measurement}"'
        if device is not None:
            predicate += f' AND device_id="{device.device_id}"'

        if start is None:
            start = datetime(1970, 1, 1, tzinfo=timezone.utc)
        if end is None:
            end = datetime(2099, 12, 31, tzinfo=timezone.utc)

        try:
            delete_api.delete(start, end, predicate, bucket=self._bucket, org=self._org)
            logger.info(
                "Deleted InfluxDB readings: measurement=%s, device=%s",
                measurement,
                device.device_id if device else "all",
            )
        except Exception:
            logger.exception("Failed to delete InfluxDB readings")
            raise

    def health_check(self):
        """Check InfluxDB connectivity.

        Returns:
            dict with "status" ("pass" or "fail") and optional "message".
        """
        try:
            health = self._client.health()
            return {
                "status": "pass" if health.status == "pass" else "fail",
                "message": health.message or "",
                "version": getattr(health, "version", ""),
            }
        except Exception as e:
            return {"status": "fail", "message": str(e)}

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

    @staticmethod
    def _measurement_name(reading_model):
        """Derive the InfluxDB measurement name from the reading model.

        Uses the device type name in lowercase (strips 'Reading' suffix).
        E.g., WeatherStationReading -> weatherstation.
        """
        name = reading_model.__name__
        if name.endswith("Reading"):
            name = name[:-7]
        return name.lower()

    @staticmethod
    def _coerce_field_value(value):
        """Ensure the value is a type InfluxDB accepts as a field value."""
        if isinstance(value, bool):
            return value
        if isinstance(value, int):
            return float(value)
        if isinstance(value, (float, str)):
            return value
        return str(value)

    def _build_flux_query(
        self,
        measurement,
        device=None,
        start=None,
        end=None,
        fields=None,
        limit=None,
        order="-timestamp",
    ):
        """Build a Flux query string for reading retrieval."""
        parts = [
            f'from(bucket: "{self._bucket}")',
            f"  |> range({self._flux_range(start, end)})",
            f'  |> filter(fn: (r) => r._measurement == "{measurement}")',
        ]

        if device is not None:
            parts.append(f'  |> filter(fn: (r) => r.device_id == "{device.device_id}")')

        if fields:
            field_filter = " or ".join(f'r._field == "{f}"' for f in fields)
            parts.append(f"  |> filter(fn: (r) => {field_filter})")

        # Sort
        desc = order.startswith("-")
        parts.append(f'  |> sort(columns: ["_time"], desc: {str(desc).lower()})')

        if limit:
            parts.append(f"  |> limit(n: {limit})")

        return "\n".join(parts)

    @staticmethod
    def _flux_range(start, end):
        """Build the range() parameters for a Flux query."""
        if start and end:
            return f"start: {start.isoformat()}, stop: {end.isoformat()}"
        elif start:
            return f"start: {start.isoformat()}"
        elif end:
            return f"start: 0, stop: {end.isoformat()}"
        else:
            # Default: last 30 days
            return "start: -30d"

    @staticmethod
    def _flux_aggregate_function(function):
        """Map aggregation function names to Flux function names."""
        mapping = {
            "avg": "mean",
            "min": "min",
            "max": "max",
            "sum": "sum",
            "count": "count",
        }
        fn = mapping.get(function)
        if fn is None:
            raise ValueError(
                f"Unknown aggregate function: {function}. "
                f"Supported: {', '.join(mapping.keys())}"
            )
        return fn

    @staticmethod
    def _normalize_flux_interval(interval):
        """Convert an interval to Flux duration syntax.

        Accepts:
            - str: "5 minutes" -> "5m", "1 hour" -> "1h", "1 day" -> "1d"
            - timedelta: converted to seconds-based duration
        """
        from datetime import timedelta as td

        if isinstance(interval, td):
            total = int(interval.total_seconds())
            if total < 60:
                return f"{total}s"
            elif total < 3600:
                return f"{total // 60}m"
            elif total < 86400:
                return f"{total // 3600}h"
            else:
                return f"{total // 86400}d"

        if isinstance(interval, str):
            # Try to parse "N unit" format
            parts = interval.strip().split()
            if len(parts) == 2:
                num, unit = parts
                unit_map = {
                    "second": "s",
                    "seconds": "s",
                    "minute": "m",
                    "minutes": "m",
                    "hour": "h",
                    "hours": "h",
                    "day": "d",
                    "days": "d",
                    "week": "w",
                    "weeks": "w",
                    "month": "mo",
                    "months": "mo",
                }
                suffix = unit_map.get(unit.lower(), unit[0])
                return f"{num}{suffix}"
            # Already in Flux format (e.g., "5m", "1h")
            return interval

        return "1h"

    @staticmethod
    def _merge_rows(rows):
        """Merge rows sharing the same (device_id, timestamp) into single dicts.

        InfluxDB returns one row per field per timestamp; this combines them.
        """
        merged = {}
        for row in rows:
            key = (row["device_id"], row["timestamp"])
            if key not in merged:
                merged[key] = {
                    "device_id": row["device_id"],
                    "timestamp": row["timestamp"],
                }
            # Add field values
            for k, v in row.items():
                if k not in ("device_id", "timestamp"):
                    merged[key][k] = v

        return list(merged.values())
