"""
Yoro model and form fields for Django.

``YoroField`` is a model field that stores a Yoro code as a ``CharField``.
It auto-encodes ``(lat, lon)`` tuples and provides the ``YoroWidget`` in forms.

Usage::

    from yoro.django.fields import YoroField

    class DeliveryPoint(models.Model):
        name = models.CharField(max_length=100)
        address = YoroField(domain="CI", precision=12)

    # Assign a code directly:
    point.address = "CI-PV9XD"

    # Or auto-encode from coordinates:
    point.address = (5.345, -4.028)
    point.save()
    print(point.address)  # "CI-PV9XD"
"""

from __future__ import annotations

from django import forms
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _

from yoro.codec import decode as yoro_decode
from yoro.codec import domain_for_country
from yoro.codec import encode as yoro_encode
from yoro.django.widgets import YoroWidget


class YoroFormField(forms.CharField):
    """Form field for Yoro codes with validation."""

    def __init__(self, *args, domain: str = "CI", **kwargs):
        self.domain = domain
        super().__init__(*args, **kwargs)

    def validate(self, value: str) -> None:
        super().validate(value)
        if not value:
            return
        try:
            yoro_decode(value)
        except ValueError as e:
            raise ValidationError(
                _("Invalid Yoro code: %(error)s"),
                code="invalid_yoro",
                params={"error": str(e)},
            )


class YoroField(models.CharField):
    """A Django model field that stores a Yoro geographic code.

    Behaves like a ``CharField`` in the database (stores the code string),
    but adds:

    - **Auto-encoding**: assign a ``(lat, lon)`` tuple and it encodes
      automatically on save.
    - **Validation**: rejects invalid codes on form submission.
    - **Widget**: renders an interactive Leaflet map picker in forms.

    Args:
        domain: ISO country code for the Yoro domain (default ``"CI"``).
        precision: Hilbert order — controls resolution (default ``12``).
        map_height: Height of the map widget in pixels (default ``300``).
        map_attrs: Extra widget configuration (``tile_url``, ``tile_attribution``).

    Example::

        class Shop(models.Model):
            name = models.CharField(max_length=100)
            address = YoroField(domain="CI", precision=12)

            # With custom map:
            address = YoroField(
                domain="CI",
                precision=14,
                map_height=400,
                map_attrs={"tile_url": "https://.../{z}/{x}/{y}.png"},
            )
    """

    description = _("Yoro geographic code")

    def __init__(
        self,
        *args,
        domain: str = "CI",
        precision: int = 12,
        map_height: int = 300,
        map_attrs: dict | None = None,
        **kwargs,
    ):
        self.yoro_domain = domain_for_country(domain)
        self.yoro_precision = precision
        self.yoro_map_height = map_height
        self.yoro_map_attrs = map_attrs or {}

        kwargs.setdefault("max_length", 20)
        kwargs.setdefault("blank", True)
        kwargs.setdefault("default", "")
        kwargs.setdefault("verbose_name", _("Yoro code"))
        super().__init__(*args, **kwargs)

    def deconstruct(self) -> tuple:
        name, path, args, kwargs = super().deconstruct()
        if self.yoro_domain != "CI":
            kwargs["domain"] = self.yoro_domain
        if self.yoro_precision != 12:
            kwargs["precision"] = self.yoro_precision
        if self.yoro_map_height != 300:
            kwargs["map_height"] = self.yoro_map_height
        if self.yoro_map_attrs:
            kwargs["map_attrs"] = self.yoro_map_attrs
        # Remove defaults we set so migrations stay clean
        for key in ("max_length", "blank", "default", "verbose_name"):
            if key in kwargs:
                defaults = {"max_length": 20, "blank": True, "default": "", "verbose_name": _("Yoro code")}
                if kwargs[key] == defaults.get(key):
                    del kwargs[key]
        return name, path, args, kwargs

    def to_python(self, value) -> str:
        """Accept strings, (lat, lon) tuples, and None."""
        if value is None:
            return ""
        if isinstance(value, (tuple, list)) and len(value) == 2:
            lat, lon = float(value[0]), float(value[1])
            return yoro_encode(lat, lon, precision=self.yoro_precision, domain=self.yoro_domain)
        return str(value).strip()

    def get_prep_value(self, value):
        """Ensure tuples are encoded before hitting the database."""
        value = self.to_python(value)
        return super().get_prep_value(value)

    def pre_save(self, model_instance, add):
        """Auto-encode (lat, lon) tuples on save."""
        value = getattr(model_instance, self.attname)
        if isinstance(value, (tuple, list)) and len(value) == 2:
            value = self.to_python(value)
            setattr(model_instance, self.attname, value)
        return value

    def validate(self, value, model_instance) -> None:
        """Validate that the value is a valid Yoro code."""
        super().validate(value, model_instance)
        if not value:
            return
        try:
            yoro_decode(value)
        except ValueError as e:
            raise ValidationError(
                _("Invalid Yoro code: %(error)s"),
                code="invalid_yoro",
                params={"error": str(e)},
            )

    def formfield(self, **kwargs) -> forms.Field:
        """Use YoroWidget with the map picker by default."""
        widget = YoroWidget(
            domain=self.yoro_domain,
            precision=self.yoro_precision,
            map_height=self.yoro_map_height,
            **self.yoro_map_attrs,
        )
        defaults = {
            "form_class": YoroFormField,
            "widget": widget,
            "domain": self.yoro_domain,
        }
        defaults.update(kwargs)
        return super().formfield(**defaults)

    # --- Helper methods available on model instances ---

    class YoroDescriptor:
        """Descriptor that provides helper methods on the field value."""

        def __init__(self, field):
            self.field = field

        def __set_name__(self, owner, name):
            self.attr_name = name

        def __get__(self, obj, objtype=None):
            if obj is None:
                return self
            return obj.__dict__.get(self.attr_name, "")

        def __set__(self, obj, value):
            if isinstance(value, (tuple, list)) and len(value) == 2:
                value = yoro_encode(
                    float(value[0]), float(value[1]),
                    precision=self.field.yoro_precision,
                    domain=self.field.yoro_domain,
                )
            obj.__dict__[self.attr_name] = value

    def contribute_to_class(self, cls, name, **kwargs):
        super().contribute_to_class(cls, name, **kwargs)
        setattr(cls, name, self.YoroDescriptor(self))
