from decimal import Decimal, ROUND_HALF_UP
from otree.i18n import 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 DecimalUnit(Decimal):

    # Default class attributes (should be overridden by subclasses)
    storage_places = None
    input_places = None
    input_unit_label = ''
    output_min_places = None
    output_max_places = None

    @staticmethod
    def output(formatted: str, raw: Decimal) -> str:
        return formatted

    def __new__(cls, amount):
        """Create an instance of this decimal unit"""
        if amount is None:
            raise ValueError('Cannot convert None to decimal')
        cls._validate_config()
        instance = Decimal.__new__(cls, cls._sanitize(amount))
        return instance

    @classmethod
    def _validate_config(cls):
        """Validate that required class attributes are defined"""
        if cls.storage_places is None:
            raise ValueError(
                f'{cls.__name__} is missing required attribute "storage_places"'
            )
        if cls.input_places is None:
            raise ValueError(
                f'{cls.__name__} is missing required attribute "input_places"'
            )
        if cls.output_min_places is None:
            raise ValueError(
                f'{cls.__name__} is missing required attribute "output_min_places"'
            )
        if cls.output_max_places is None:
            raise ValueError(
                f'{cls.__name__} is missing required attribute "output_max_places"'
            )

    @classmethod
    def _sanitize(cls, amount):
        if isinstance(amount, cls):
            return amount
        places = cls.storage_places
        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),))

    # 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):
        # don't use .normalize() because:
        # (1) easier to understand the precision
        # (2) numbers line up, e.g. in data table view.
        return f'{self.__class__.__name__}({Decimal(self)})'

    def __eq__(self, other):
        if isinstance(other, DecimalUnit):
            # Only equal if same type and same value
            return type(self) == type(other) and Decimal.__eq__(self, other)
        elif isinstance(other, (int, float, Decimal)):
            return Decimal.__eq__(self, self._sanitize(other))
        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)

        return self.__class__.output(result, Decimal(self))

    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 unit's display settings"""
        min_places = self.__class__.output_min_places
        max_places = self.__class__.output_max_places
        return self._format_with_places(min_places, max_places)

    def to_real_world_currency(self, session):
        """
        Compatibility method for when CurrencyField is aliased to DecimalField.
        Since there's only one currency type when CURRENCY_UNIT is defined,
        just return self (similar to Currency.to_real_world_currency when USE_POINTS=False)
        """
        return self

    def _make_result(self, result, other):
        """DRY helper for arithmetic operations"""
        if not isinstance(other, DecimalUnit):
            # Operating with scalar preserves type
            return self.__class__(result)
        if type(self) == type(other):
            # Same type preserves type
            return self.__class__(result)
        # Different types 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 self.__class__(Decimal.__neg__(self))

    def __pos__(self):
        return self.__class__(Decimal.__pos__(self))

    def __abs__(self):
        return self.__class__(Decimal.__abs__(self))
