from decimal import Decimal, ROUND_HALF_UP
from otree import settings
from otree.i18n import format_number, convert_decimal_separator


def _to_decimal(amount):
    if isinstance(amount, Decimal):
        return amount
    elif isinstance(amount, float):
        return Decimal.from_float(amount)
    else:
        return Decimal(amount)


class DecimalValue(Decimal):
    """
    Generalized decimal type with config-based precision and formatting.
    Completely separate from Currency for backward compatibility.

    Usage:
        score = DecimalValue(0.856, config='percent')
        hours = dec(2.5, config='hours')  # Using convenience function
    """

    def __new__(cls, amount, config):
        if amount is None:
            raise ValueError('Cannot convert None to decimal')
        if config not in settings.DECIMAL_CONFIGS_DICT:
            raise ValueError(f'Unknown decimal config: {config}')
        instance = Decimal.__new__(cls, cls._sanitize(amount, config))
        instance._config = config
        return instance

    @classmethod
    def _sanitize(cls, amount, config):
        if isinstance(amount, DecimalValue) and amount._config == config:
            return amount
        cfg = settings.DECIMAL_CONFIGS_DICT[config]
        try:
            places = cfg['places']
        except KeyError:
            msg = f"Config '{config}' is missing required key 'places'"
            raise ValueError(msg)
        quant = Decimal('0.1') ** places
        return _to_decimal(amount).quantize(quant, rounding=ROUND_HALF_UP)

    # Support for pickling
    def __reduce__(self):
        return (self.__class__, (Decimal.__str__(self), self._config))

    # Immutable
    def __copy__(self):
        return self

    def __deepcopy__(self, memo):
        return self

    def __float__(self):
        return float(Decimal(self))

    def __str__(self):
        return self._format_for_display()

    def __repr__(self):
        return f'{Decimal(self).normalize()}{self._config}'

    def __eq__(self, other):
        if isinstance(other, DecimalValue):
            # Only equal if same config and same value
            return self._config == other._config and Decimal.__eq__(self, other)
        elif isinstance(other, (int, float, Decimal)):
            return Decimal.__eq__(self, self._sanitize(other, self._config))
        else:
            return False

    __hash__ = Decimal.__hash__

    def _format_with_places(self, min_places, max_places):
        """Helper to format with specific decimal places and apply custom function"""
        # Round to max_places
        quant = Decimal('0.1') ** max_places
        rounded = Decimal(self).quantize(quant, rounding=ROUND_HALF_UP)

        # Format with Python's thousand separator (comma) and max_places decimals
        fmt = "{:,.%df}" % max_places
        result = fmt.format(rounded)

        # Strip trailing zeros down to min_places (before converting separators)
        if min_places < max_places and '.' in result:
            parts = result.split('.')
            decimal_part = parts[1].rstrip('0')
            # Ensure we keep at least min_places
            decimal_part = decimal_part.ljust(min_places, '0')
            if decimal_part:
                result = parts[0] + '.' + decimal_part
            else:
                result = parts[0]

        # Convert ',' to THOUSAND_SEPARATOR and '.' to DECIMAL_SEPARATOR
        result = convert_decimal_separator(result)

        # Apply custom function if defined
        cfg = settings.DECIMAL_CONFIGS_DICT[self._config]
        try:
            display_cfg = cfg['display']
        except KeyError:
            msg = f"Config '{self._config}' is missing required key 'display'"
            raise ValueError(msg)
        custom_func = display_cfg.get('function')
        if custom_func:
            result = custom_func(result, self)

        return result

    def _prepare_operand(self, other):
        """Convert operand to Decimal for arithmetic"""
        if isinstance(other, Decimal):
            return other
        return _to_decimal(other)

    @staticmethod
    def _make_binary_operator(name):
        """Factory for creating arithmetic operator methods"""
        method = getattr(Decimal, name, None)

        def binary_function(self, other):
            other = self._prepare_operand(other)
            result = method(self, other)
            return self._make_result(result, other)

        return binary_function

    def _format_for_display(self):
        """Format using config's display settings"""
        cfg = settings.DECIMAL_CONFIGS_DICT[self._config]
        try:
            display_config = cfg['display']
        except KeyError:
            msg = f"Config '{self._config}' is missing required key 'display'"
            raise ValueError(msg)
        try:
            min_places = display_config['min_places']
            max_places = display_config['max_places']
        except KeyError as e:
            msg = f"Config '{self._config}' display is missing required key {e}"
            raise ValueError(msg)
        return self._format_with_places(min_places, max_places)

    def _make_result(self, result, other):
        """DRY helper for arithmetic operations"""
        if not isinstance(other, DecimalValue):
            # Operating with scalar preserves config
            return DecimalValue(result, config=self._config)
        if self._config == other._config:
            # Same config preserves config
            return DecimalValue(result, config=self._config)
        # Different configs return plain Decimal
        return Decimal(result)

    # Arithmetic operations - generated by _make_binary_operator
    __add__ = _make_binary_operator('__add__')
    __radd__ = _make_binary_operator('__radd__')
    __sub__ = _make_binary_operator('__sub__')
    __rsub__ = _make_binary_operator('__rsub__')
    __mul__ = _make_binary_operator('__mul__')
    __rmul__ = _make_binary_operator('__rmul__')
    __truediv__ = _make_binary_operator('__truediv__')
    __rtruediv__ = _make_binary_operator('__rtruediv__')
    __floordiv__ = _make_binary_operator('__floordiv__')
    __rfloordiv__ = _make_binary_operator('__rfloordiv__')
    __mod__ = _make_binary_operator('__mod__')
    __rmod__ = _make_binary_operator('__rmod__')
    __pow__ = _make_binary_operator('__pow__')
    __rpow__ = _make_binary_operator('__rpow__')

    def __neg__(self):
        return DecimalValue(Decimal.__neg__(self), config=self._config)

    def __pos__(self):
        return DecimalValue(Decimal.__pos__(self), config=self._config)

    def __abs__(self):
        return DecimalValue(Decimal.__abs__(self), config=self._config)


def dec(amount, config):
    """Convenience constructor for DecimalValue"""
    return DecimalValue(amount, config=config)
