import json
import time
from abc import abstractmethod
from enum import Enum
from sqlite3 import Row
from typing import Any, List, Optional, Tuple

from pydantic import BaseModel

from lnbits.utils.exchange_rates import btc_price, fiat_amount_as_satoshis

from .helpers import (
    decrypt_message,
    encrypt_message,
    get_shared_secret,
    sign_message_hash,
)
from .nostr.event import NostrEvent

######################################## NOSTR ########################################


class Nostrable:
    @abstractmethod
    def to_nostr_event(self, pubkey: str) -> NostrEvent:
        pass

    @abstractmethod
    def to_nostr_delete_event(self, pubkey: str) -> NostrEvent:
        pass


######################################## MERCHANT ########################################


class MerchantProfile(BaseModel):
    name: Optional[str]
    about: Optional[str]
    picture: Optional[str]


class MerchantConfig(MerchantProfile):
    event_id: Optional[str]
    sync_from_nostr = False
    active: bool = False
    restore_in_progress: Optional[bool] = False


class PartialMerchant(BaseModel):
    private_key: str
    public_key: str
    config: MerchantConfig = MerchantConfig()


class Merchant(PartialMerchant, Nostrable):
    id: str
    time: Optional[int] = 0

    def sign_hash(self, hash: bytes) -> str:
        return sign_message_hash(self.private_key, hash)

    def decrypt_message(self, encrypted_message: str, public_key: str) -> str:
        encryption_key = get_shared_secret(self.private_key, public_key)
        return decrypt_message(encrypted_message, encryption_key)

    def encrypt_message(self, clear_text_message: str, public_key: str) -> str:
        encryption_key = get_shared_secret(self.private_key, public_key)
        return encrypt_message(clear_text_message, encryption_key)

    def build_dm_event(self, message: str, to_pubkey: str) -> NostrEvent:
        content = self.encrypt_message(message, to_pubkey)
        event = NostrEvent(
            pubkey=self.public_key,
            created_at=round(time.time()),
            kind=4,
            tags=[["p", to_pubkey]],
            content=content,
        )
        event.id = event.event_id
        event.sig = self.sign_hash(bytes.fromhex(event.id))

        return event

    @classmethod
    def from_row(cls, row: Row) -> "Merchant":
        merchant = cls(**dict(row))
        merchant.config = MerchantConfig(**json.loads(row["meta"]))
        return merchant

    def to_nostr_event(self, pubkey: str) -> NostrEvent:
        content = {
            "name": self.config.name,
            "about": self.config.about,
            "picture": self.config.picture,
        }
        event = NostrEvent(
            pubkey=pubkey,
            created_at=round(time.time()),
            kind=0,
            tags=[],
            content=json.dumps(content, separators=(",", ":"), ensure_ascii=False),
        )
        event.id = event.event_id

        return event

    def to_nostr_delete_event(self, pubkey: str) -> NostrEvent:
        content = {
            "name": f"{self.config.name} (deleted)",
            "about": "Merchant Deleted",
            "picture": "",
        }
        delete_event = NostrEvent(
            pubkey=pubkey,
            created_at=round(time.time()),
            kind=5,
            tags=[],
            content=json.dumps(content, separators=(",", ":"), ensure_ascii=False),
        )
        delete_event.id = delete_event.event_id

        return delete_event


######################################## ZONES ########################################
class PartialZone(BaseModel):
    id: Optional[str]
    name: Optional[str]
    currency: str
    cost: float
    countries: List[str] = []


class Zone(PartialZone):
    id: str

    @classmethod
    def from_row(cls, row: Row) -> "Zone":
        zone = cls(**dict(row))
        zone.countries = json.loads(row["regions"])
        return zone


######################################## STALLS ########################################


class StallConfig(BaseModel):
    image_url: Optional[str]
    description: Optional[str]


class PartialStall(BaseModel):
    id: Optional[str]
    wallet: str
    name: str
    currency: str = "sat"
    shipping_zones: List[Zone] = []
    config: StallConfig = StallConfig()
    pending: bool = False

    """Last published nostr event for this Stall"""
    event_id: Optional[str]
    event_created_at: Optional[int]

    def validate_stall(self):
        for z in self.shipping_zones:
            if z.currency != self.currency:
                raise ValueError(
                    f"Sipping zone '{z.name}' has different currency than stall."
                )


class Stall(PartialStall, Nostrable):
    id: str

    def to_nostr_event(self, pubkey: str) -> NostrEvent:
        content = {
            "id": self.id,
            "name": self.name,
            "description": self.config.description,
            "currency": self.currency,
            "shipping": [dict(z) for z in self.shipping_zones],
        }
        event = NostrEvent(
            pubkey=pubkey,
            created_at=round(time.time()),
            kind=30017,
            tags=[["d", self.id]],
            content=json.dumps(content, separators=(",", ":"), ensure_ascii=False),
        )
        event.id = event.event_id

        return event

    def to_nostr_delete_event(self, pubkey: str) -> NostrEvent:
        delete_event = NostrEvent(
            pubkey=pubkey,
            created_at=round(time.time()),
            kind=5,
            tags=[["e", self.event_id]],
            content=f"Stall '{self.name}' deleted",
        )
        delete_event.id = delete_event.event_id

        return delete_event

    @classmethod
    def from_row(cls, row: Row) -> "Stall":
        stall = cls(**dict(row))
        stall.config = StallConfig(**json.loads(row["meta"]))
        stall.shipping_zones = [Zone(**z) for z in json.loads(row["zones"])]
        return stall


######################################## PRODUCTS ########################################


class ProductShippingCost(BaseModel):
    id: str
    cost: int


class ProductConfig(BaseModel):
    description: Optional[str]
    currency: Optional[str]
    use_autoreply: Optional[bool] = False
    autoreply_message: Optional[str]
    shipping: Optional[List[ProductShippingCost]] = []


class PartialProduct(BaseModel):
    id: Optional[str]
    stall_id: str
    name: str
    categories: List[str] = []
    images: List[str] = []
    price: float
    quantity: int
    pending: bool = False
    config: ProductConfig = ProductConfig()

    """Last published nostr event for this Product"""
    event_id: Optional[str]
    event_created_at: Optional[int]


class Product(PartialProduct, Nostrable):
    id: str

    def to_nostr_event(self, pubkey: str) -> NostrEvent:
        content = {
            "id": self.id,
            "stall_id": self.stall_id,
            "name": self.name,
            "description": self.config.description,
            "images": self.images,
            "currency": self.config.currency,
            "price": self.price,
            "quantity": self.quantity,
            "shipping": [dict(s) for s in self.config.shipping or []]
        }
        categories = [["t", tag] for tag in self.categories]

        event = NostrEvent(
            pubkey=pubkey,
            created_at=round(time.time()),
            kind=30018,
            tags=[["d", self.id]] + categories,
            content=json.dumps(content, separators=(",", ":"), ensure_ascii=False),
        )
        event.id = event.event_id

        return event

    def to_nostr_delete_event(self, pubkey: str) -> NostrEvent:
        delete_event = NostrEvent(
            pubkey=pubkey,
            created_at=round(time.time()),
            kind=5,
            tags=[["e", self.event_id]],
            content=f"Product '{self.name}' deleted",
        )
        delete_event.id = delete_event.event_id

        return delete_event

    @classmethod
    def from_row(cls, row: Row) -> "Product":
        product = cls(**dict(row))
        product.config = ProductConfig(**json.loads(row["meta"]))
        product.images = json.loads(row["image_urls"]) if "image_urls" in row else []
        product.categories = json.loads(row["category_list"])
        return product


class ProductOverview(BaseModel):
    id: str
    name: str
    price: float


######################################## ORDERS ########################################


class OrderItem(BaseModel):
    product_id: str
    quantity: int


class OrderContact(BaseModel):
    nostr: Optional[str]
    phone: Optional[str]
    email: Optional[str]


class OrderExtra(BaseModel):
    products: List[ProductOverview]
    currency: str
    btc_price: str
    shipping_cost: float = 0
    shipping_cost_sat: float = 0
    fail_message: Optional[str]

    @classmethod
    async def from_products(cls, products: List[Product]):
        currency = products[0].config.currency if len(products) else "sat"
        exchange_rate = (
            (await btc_price(currency)) if currency and currency != "sat" else 1
        )
        return OrderExtra(products=products, currency=currency, btc_price=exchange_rate)


class PartialOrder(BaseModel):
    id: str
    event_id: Optional[str]
    event_created_at: Optional[int]
    public_key: str
    merchant_public_key: str
    shipping_id: str
    items: List[OrderItem]
    contact: Optional[OrderContact]
    address: Optional[str]

    def validate_order(self):
        assert len(self.items) != 0, f"Order has no items. Order: '{self.id}'"

    def validate_order_items(self, product_list: List[Product]):
        assert len(self.items) != 0, f"Order has no items. Order: '{self.id}'"
        assert (
            len(product_list) != 0
        ), f"No products found for order. Order: '{self.id}'"

        product_ids = [p.id for p in product_list]
        for item in self.items:
            if item.product_id not in product_ids:
                raise ValueError(
                    f"Order ({self.id}) item product does not exist: {item.product_id}"
                )

        stall_id = product_list[0].stall_id
        for p in product_list:
            if p.stall_id != stall_id:
                raise ValueError(
                    f"Order ({self.id}) has products from different stalls"
                )

    async def costs_in_sats(
        self, products: List[Product], shipping_id: str, stall_shipping_cost: float
    ) -> Tuple[float, float]:
        product_prices = {}
        for p in products:
            product_shipping_cost = next(
                (s.cost for s in p.config.shipping if s.id == shipping_id), 0
            )
            product_prices[p.id] = {
                "price": p.price + product_shipping_cost,
                "currency": p.config.currency or "sat",
            }

        product_cost: float = 0  # todo
        for item in self.items:
            assert item.quantity > 0, "Quantity cannot be negative"
            price = product_prices[item.product_id]["price"]
            currency = product_prices[item.product_id]["currency"]
            if currency != "sat":
                price = await fiat_amount_as_satoshis(price, currency)
            product_cost += item.quantity * price

        if currency != "sat":
            stall_shipping_cost = await fiat_amount_as_satoshis(
                stall_shipping_cost, currency
            )

        return product_cost, stall_shipping_cost

    def receipt(
        self, products: List[Product], shipping_id: str, stall_shipping_cost: float
    ) -> str:
        if len(products) == 0:
            return "[No Products]"
        receipt = ""
        product_prices = {}
        for p in products:
            product_shipping_cost = next(
                (s.cost for s in p.config.shipping if s.id == shipping_id), 0
            )
            product_prices[p.id] = {
                "name": p.name,
                "price": p.price,
                "product_shipping_cost": product_shipping_cost
            }

        currency = products[0].config.currency or "sat"
        products_cost: float = 0  # todo
        items_receipts = []
        for item in self.items:
            prod = product_prices[item.product_id]
            price = prod["price"] + prod["product_shipping_cost"]

            products_cost += item.quantity * price

            items_receipts.append(f"""[{prod["name"]}:  {item.quantity} x ({prod["price"]} + {prod["product_shipping_cost"]}) = {item.quantity * price} {currency}] """)

        receipt = "; ".join(items_receipts)
        receipt += f"[Products cost: {products_cost} {currency}] [Stall shipping cost: {stall_shipping_cost} {currency}]; "
        receipt += f"[Total: {products_cost + stall_shipping_cost} {currency}]"

        return receipt


class Order(PartialOrder):
    stall_id: str
    invoice_id: str
    total: float
    paid: bool = False
    shipped: bool = False
    time: Optional[int]
    extra: OrderExtra

    @classmethod
    def from_row(cls, row: Row) -> "Order":
        contact = OrderContact(**json.loads(row["contact_data"]))
        extra = OrderExtra(**json.loads(row["extra_data"]))
        items = [OrderItem(**z) for z in json.loads(row["order_items"])]
        order = cls(**dict(row), contact=contact, items=items, extra=extra)
        return order


class OrderStatusUpdate(BaseModel):
    id: str
    message: Optional[str]
    paid: Optional[bool]
    shipped: Optional[bool]


class OrderReissue(BaseModel):
    id: str
    shipping_id: Optional[str] = None


class PaymentOption(BaseModel):
    type: str
    link: str


class PaymentRequest(BaseModel):
    id: str
    message: Optional[str]
    payment_options: List[PaymentOption]


######################################## MESSAGE ########################################


class DirectMessageType(Enum):
    """Various types os direct messages."""

    PLAIN_TEXT = -1
    CUSTOMER_ORDER = 0
    PAYMENT_REQUEST = 1
    ORDER_PAID_OR_SHIPPED = 2


class PartialDirectMessage(BaseModel):
    event_id: Optional[str]
    event_created_at: Optional[int]
    message: str
    public_key: str
    type: int = DirectMessageType.PLAIN_TEXT.value
    incoming: bool = False
    time: Optional[int]

    @classmethod
    def parse_message(cls, msg) -> Tuple[DirectMessageType, Optional[Any]]:
        try:
            msg_json = json.loads(msg)
            if "type" in msg_json:
                return DirectMessageType(msg_json["type"]), msg_json

            return DirectMessageType.PLAIN_TEXT, None
        except Exception:
            return DirectMessageType.PLAIN_TEXT, None


class DirectMessage(PartialDirectMessage):
    id: str

    @classmethod
    def from_row(cls, row: Row) -> "DirectMessage":
        dm = cls(**dict(row))
        return dm


######################################## CUSTOMERS ########################################


class CustomerProfile(BaseModel):
    name: Optional[str]
    about: Optional[str]


class Customer(BaseModel):
    merchant_id: str
    public_key: str
    event_created_at: Optional[int]
    profile: Optional[CustomerProfile]
    unread_messages: int = 0

    @classmethod
    def from_row(cls, row: Row) -> "Customer":
        customer = cls(**dict(row))
        customer.profile = (
            CustomerProfile(**json.loads(row["meta"])) if "meta" in row else None
        )
        return customer
