from dataclasses import asdict, dataclass, field
from decimal import Decimal
from typing import Any

from app.core.exceptions import ValidationError
from app.models import Order, Product
from app.utils.helpers import money
from app.utils.validators import (
    clean_amount,
    clean_channel,
    clean_int,
    clean_text,
    clean_ton_address,
    clean_username,
)


@dataclass
class Step:
    """One piece of user input a flow needs before checkout."""

    key: str
    type: str
    prompt: str
    optional: bool = False
    config: dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        return asdict(self)


@dataclass
class Option:
    """A toggle/choice shown on the confirmation screen (e.g. gift hide)."""

    key: str
    type: str
    label: str
    default: Any = None
    config: dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        return asdict(self)


class FlowContext:
    """Services a flow may use while validating, pricing and processing."""

    def __init__(self, settings_service: Any, fragment: Any = None, ton: Any = None) -> None:
        self.settings = settings_service
        self.fragment = fragment
        self.ton = ton


class BaseFlow:
    """Shared purchase behaviour. Child flows only add product-specific bits."""

    name = "base"
    steps: list[Step] = []
    options: list[Option] = []
    #: flows without a catalog product (wallet top-up)
    productless = False

    def get_steps(self, product: Product | None) -> list[Step]:
        steps = list(self.steps)
        if product is not None and bool((product.data or {}).get("per_unit")):
            steps.insert(
                0,
                Step(
                    key="quantity",
                    type="number",
                    prompt="input_quantity",
                    config={
                        "min": int((product.data or {}).get("min_quantity", 1)),
                        "max": int((product.data or {}).get("max_quantity", 100000)),
                        "balance_calc": True,
                    },
                ),
            )
        return steps

    def get_options(self, product: Product | None) -> list[Option]:
        return list(self.options)

    # -- validation -------------------------------------------------
    def validate(self, step: Step, value: Any) -> Any:
        if step.type == "username":
            return clean_username(str(value))
        if step.type == "channel":
            return clean_channel(str(value))
        if step.type == "ton_address":
            return clean_ton_address(str(value))
        if step.type == "number":
            return clean_int(
                value,
                minimum=int(step.config.get("min", 1)),
                maximum=int(step.config.get("max", 1_000_000)),
            )
        if step.type == "amount":
            return str(
                clean_amount(
                    value,
                    minimum=Decimal(str(step.config.get("min", "0.000001"))),
                    maximum=Decimal(str(step.config.get("max", "1000000"))),
                )
            )
        if step.type == "text":
            return clean_text(str(value), int(step.config.get("max_length", 200)))
        raise ValidationError("unknown_step_type")

    def validate_all(self, product: Product | None, input_data: dict[str, Any]) -> dict[str, Any]:
        cleaned: dict[str, Any] = dict(input_data)
        for step in self.get_steps(product):
            raw = input_data.get(step.key)
            if raw in (None, ""):
                if step.optional:
                    cleaned.pop(step.key, None)
                    continue
                raise ValidationError(f"missing_{step.key}")
            cleaned[step.key] = self.validate(step, raw)
        return cleaned

    # -- pricing ----------------------------------------------------
    def quantity(self, product: Product | None, input_data: dict[str, Any]) -> int:
        if product is not None and bool((product.data or {}).get("per_unit")):
            return int(input_data.get("quantity") or 1)
        return 1

    async def base_price(
        self, ctx: FlowContext, product: Product | None, input_data: dict[str, Any]
    ) -> Decimal:
        if product is None:
            raise ValidationError("product_required")
        return money(product.effective_price * self.quantity(product, input_data))

    async def extra_price(
        self, ctx: FlowContext, product: Product | None, input_data: dict[str, Any]
    ) -> Decimal:
        return Decimal("0")

    async def calculate_price(
        self, ctx: FlowContext, product: Product | None, input_data: dict[str, Any]
    ) -> Decimal:
        base = await self.base_price(ctx, product, input_data)
        return money(base + await self.extra_price(ctx, product, input_data))

    # -- display ----------------------------------------------------
    def summary(self, order: Order) -> dict[str, Any]:
        """Key/value pairs (message key -> value) rendered on the invoice."""
        data = order.input_data or {}
        rows: dict[str, Any] = {}
        for key in ("username", "channel", "wallet", "amount", "memo", "quantity"):
            if data.get(key) not in (None, ""):
                rows[key] = data[key]
        return rows

    # -- fulfilment -------------------------------------------------
    async def process(self, ctx: FlowContext, order: Order) -> dict[str, Any]:
        """Deliver the product after successful payment.

        Returns a result payload. Raising means the order is marked failed.
        The default keeps the order in manual processing for the admin.
        """
        return {"manual": True}
