import datetime
import decimal
import json
from collections.abc import Iterable
from typing import Any

from django.conf import settings
from django.db import models
from django.db.models import Model
from django.template.loader import render_to_string
from django.utils import formats, timezone
from django.utils.hashable import make_hashable
from django.utils.html import format_html
from django.utils.safestring import SafeText, mark_safe

from .exceptions import UnfoldException

try:
    from djmoney.models.fields import MoneyField
    from djmoney.money import Money
except ImportError:
    MoneyField = None
    Money = None


def _boolean_icon(field_val: Any) -> str:
    return render_to_string("unfold/helpers/boolean.html", {"value": field_val})


def display_for_header(value: Iterable, empty_value_display: str) -> SafeText:
    if not isinstance(value, list) and not isinstance(value, tuple):
        raise UnfoldException("Display header requires list or tuple")

    return mark_safe(
        render_to_string(
            "unfold/helpers/display_header.html",
            {
                "value": value,
            },
        )
    )


def display_for_dropdown(
    result: Model, field_name: str, value: Iterable, empty_value_display: str
) -> SafeText:
    return render_to_string(
        "unfold/helpers/display_dropdown.html",
        {
            "instance": result,
            "field_name": field_name,
            "value": value,
        },
    )


def display_for_label(value: Any, empty_value_display: str, label: Any) -> SafeText:
    label_type = None
    multiple = False

    if isinstance(label, dict):
        if isinstance(value, tuple):
            try:
                label_type = label[value[0]]
                value = value[1]
            except KeyError:
                value = value[0]
        elif value in label:
            label_type = label[value]

    if isinstance(value, tuple) or isinstance(value, list):
        multiple = True

    return mark_safe(
        render_to_string(
            "unfold/helpers/display_label.html",
            {
                "label": value,
                "label_type": label_type,
                "multiple": multiple,
            },
        )
    )


def display_for_value(
    value: Any, empty_value_display: str, boolean: bool = False
) -> str:
    if boolean:
        return _boolean_icon(value)
    elif value is None:
        return empty_value_display
    elif isinstance(value, bool):
        return str(value)
    elif isinstance(value, datetime.datetime):
        return formats.localize(timezone.template_localtime(value))
    elif isinstance(value, datetime.date | datetime.time):
        return formats.localize(value)
    elif Money is not None and isinstance(value, Money):
        return str(value)
    elif isinstance(value, int | decimal.Decimal | float):
        return formats.number_format(value)
    elif isinstance(value, list | tuple):
        return ", ".join(str(v) for v in value)
    else:
        return str(value)


def display_for_field(value: Any, field: Any, empty_value_display: str) -> str:
    if getattr(field, "flatchoices", None):
        try:
            return dict(field.flatchoices).get(value, empty_value_display)
        except TypeError:
            # Allow list-like choices.
            flatchoices = make_hashable(field.flatchoices)
            value = make_hashable(value)
            return dict(flatchoices).get(value, empty_value_display)
    elif isinstance(field, models.BooleanField):
        return _boolean_icon(value)
    elif value is None or value == "":
        return empty_value_display
    elif isinstance(field, models.DateTimeField):
        return formats.localize(timezone.template_localtime(value))
    elif isinstance(field, models.DateField | models.TimeField):
        return formats.localize(value)
    elif MoneyField is not None and isinstance(field, MoneyField):
        return str(value)
    elif isinstance(field, models.DecimalField):
        return formats.number_format(value, field.decimal_places)
    elif isinstance(field, models.IntegerField | models.FloatField):
        return formats.number_format(value)
    elif isinstance(field, models.FileField) and value:
        return format_html('<a href="{}">{}</a>', value.url, value)
    elif isinstance(field, models.JSONField) and value:
        try:
            return json.dumps(value, ensure_ascii=False, cls=field.encoder)
        except TypeError:
            return display_for_value(value, empty_value_display)
    else:
        return display_for_value(value, empty_value_display)


def prettify_json(data: Any, encoder: Any) -> str | None:
    try:
        from pygments import highlight
        from pygments.formatters import HtmlFormatter
        from pygments.lexers import JsonLexer
    except ImportError:
        return None

    def format_response(response: str, theme: str) -> str:
        formatter = HtmlFormatter(
            style=theme,
            noclasses=True,
            nobackground=True,
            prestyles="white-space: pre-wrap; word-wrap: break-word;",
        )
        return highlight(response, JsonLexer(), formatter)

    response = json.dumps(data, sort_keys=True, indent=4, cls=encoder)

    return mark_safe(
        f'<div class="block dark:hidden">{format_response(response, "colorful")}</div>'
        f'<div class="hidden dark:block">{format_response(response, "monokai")}</div>'
    )


def parse_date_str(value: str) -> datetime.date | None:
    for format in settings.DATE_INPUT_FORMATS:
        try:
            return datetime.datetime.strptime(value, format).date()
        except (ValueError, TypeError):
            continue


def parse_datetime_str(value: str) -> datetime.datetime | None:
    for format in settings.DATETIME_INPUT_FORMATS:
        try:
            return datetime.datetime.strptime(value, format)
        except (ValueError, TypeError):
            continue


def hex_to_rgb(hex_color: str) -> list[int]:
    hex_color = hex_color.lstrip("#")

    r = int(hex_color[0:2], 16)
    g = int(hex_color[2:4], 16)
    b = int(hex_color[4:6], 16)

    return (r, g, b)


def hex_to_values(value: str) -> str:
    return ", ".join(str(item) for item in hex_to_rgb(value))


def convert_color(value: str) -> str:
    if value[0] == "#":
        return f"rgb({hex_to_values(value)})"
    elif value.startswith("rgb") or value.startswith("oklch"):
        return value
    elif isinstance(value, str) and all(part.isdigit() for part in value.split()):
        return f"rgb({', '.join(value.split(' '))})"
    elif isinstance(value, str) and all(
        part.strip().isdigit() for part in value.split(",")
    ):
        return f"rgb({', '.join(part.strip() for part in value.split(','))})"

    return value
