import secrets
from datetime import date, datetime
from typing import List, Optional, Union

from lnbits.helpers import urlsafe_short_hash

from . import db
from .models import Card, CreateCardData, Hit, Refund


async def create_card(data: CreateCardData, wallet_id: str) -> Card:
    card_id = urlsafe_short_hash().upper()
    extenal_id = urlsafe_short_hash().lower()

    await db.execute(
        """
        INSERT INTO boltcards.cards (
            id,
            uid,
            external_id,
            wallet,
            card_name,
            counter,
            tx_limit,
            daily_limit,
            enable,
            k0,
            k1,
            k2,
            otp
        )
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        """,
        (
            card_id,
            data.uid.upper(),
            extenal_id,
            wallet_id,
            data.card_name,
            data.counter,
            data.tx_limit,
            data.daily_limit,
            True,
            data.k0,
            data.k1,
            data.k2,
            secrets.token_hex(16),
        ),
    )
    card = await get_card(card_id)
    assert card, "Newly created card couldn't be retrieved"
    return card


async def update_card(card_id: str, **kwargs) -> Optional[Card]:
    if "is_unique" in kwargs:
        kwargs["is_unique"] = int(kwargs["is_unique"])
    if "uid" in kwargs:
        kwargs["uid"] = kwargs["uid"].upper()
    q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
    await db.execute(
        f"UPDATE boltcards.cards SET {q} WHERE id = ?",
        (*kwargs.values(), card_id),
    )
    row = await db.fetchone("SELECT * FROM boltcards.cards WHERE id = ?", (card_id,))
    return Card(**row) if row else None


async def get_cards(wallet_ids: Union[str, List[str]]) -> List[Card]:
    if isinstance(wallet_ids, str):
        wallet_ids = [wallet_ids]

    q = ",".join(["?"] * len(wallet_ids))
    rows = await db.fetchall(
        f"SELECT * FROM boltcards.cards WHERE wallet IN ({q})", (*wallet_ids,)
    )

    return [Card(**row) for row in rows]


async def get_card(card_id: str) -> Optional[Card]:
    row = await db.fetchone("SELECT * FROM boltcards.cards WHERE id = ?", (card_id,))
    if not row:
        return None

    card = dict(**row)

    return Card.parse_obj(card)


async def get_card_by_uid(card_uid: str) -> Optional[Card]:
    row = await db.fetchone(
        "SELECT * FROM boltcards.cards WHERE uid = ?", (card_uid.upper(),)
    )
    if not row:
        return None

    card = dict(**row)

    return Card.parse_obj(card)


async def get_card_by_external_id(external_id: str) -> Optional[Card]:
    row = await db.fetchone(
        "SELECT * FROM boltcards.cards WHERE external_id = ?", (external_id.lower(),)
    )
    if not row:
        return None

    card = dict(**row)

    return Card.parse_obj(card)


async def get_card_by_otp(otp: str) -> Optional[Card]:
    row = await db.fetchone("SELECT * FROM boltcards.cards WHERE otp = ?", (otp,))
    if not row:
        return None

    card = dict(**row)

    return Card.parse_obj(card)


async def delete_card(card_id: str) -> None:
    # Delete cards
    card = await get_card(card_id)
    await db.execute("DELETE FROM boltcards.cards WHERE id = ?", (card_id,))
    # Delete hits
    hits = await get_hits([card_id])
    for hit in hits:
        await db.execute("DELETE FROM boltcards.hits WHERE id = ?", (hit.id,))
        # Delete refunds
        refunds = await get_refunds([hit])
        for refund in refunds:
            await db.execute(
                "DELETE FROM boltcards.refunds WHERE id = ?", (refund.hit_id,)
            )


async def update_card_counter(counter: int, id: str):
    await db.execute(
        "UPDATE boltcards.cards SET counter = ? WHERE id = ?",
        (counter, id),
    )


async def enable_disable_card(enable: bool, id: str) -> Optional[Card]:
    row = await db.execute(
        "UPDATE boltcards.cards SET enable = ? WHERE id = ?",
        (enable, id),
    )
    return await get_card(id)


async def update_card_otp(otp: str, id: str):
    await db.execute(
        "UPDATE boltcards.cards SET otp = ? WHERE id = ?",
        (otp, id),
    )


async def get_hit(hit_id: str) -> Optional[Hit]:
    row = await db.fetchone(f"SELECT * FROM boltcards.hits WHERE id = ?", (hit_id))
    if not row:
        return None

    hit = dict(**row)

    return Hit.parse_obj(hit)


async def get_hits(cards_ids: Union[str, List[str]]) -> List[Hit]:
    q = ",".join(["?"] * len(cards_ids))
    rows = await db.fetchall(
        f"SELECT * FROM boltcards.hits WHERE card_id IN ({q})", (*cards_ids,)
    )

    return [Hit(**row) for row in rows]


async def get_hits_today(card_id: str) -> Optional[Hit]:
    rows = await db.fetchall(
        f"SELECT * FROM boltcards.hits WHERE card_id = ?",
        (card_id,),
    )
    updatedrow = []
    for row in rows:
        if datetime.now().date() == datetime.fromtimestamp(row.time).date():
            updatedrow.append(row)

    return [Hit(**row) for row in updatedrow]


async def spend_hit(id: str, amount: int):
    await db.execute(
        "UPDATE boltcards.hits SET spent = ?, amount = ? WHERE id = ?",
        (True, amount, id),
    )
    return await get_hit(id)


async def create_hit(card_id, ip, useragent, old_ctr, new_ctr) -> Hit:
    hit_id = urlsafe_short_hash()
    await db.execute(
        """
        INSERT INTO boltcards.hits (
            id,
            card_id,
            ip,
            spent,
            useragent,
            old_ctr,
            new_ctr,
            amount
        )
        VALUES (?, ?, ?, ?, ?, ?, ?, ?)
        """,
        (
            hit_id,
            card_id,
            ip,
            False,
            useragent,
            old_ctr,
            new_ctr,
            0,
        ),
    )
    hit = await get_hit(hit_id)
    assert hit, "Newly recorded hit couldn't be retrieved"
    return hit


async def create_refund(hit_id, refund_amount) -> Refund:
    refund_id = urlsafe_short_hash()
    await db.execute(
        """
        INSERT INTO boltcards.refunds (
            id,
            hit_id,
            refund_amount
        )
        VALUES (?, ?, ?)
        """,
        (
            refund_id,
            hit_id,
            refund_amount,
        ),
    )
    refund = await get_refund(refund_id)
    assert refund, "Newly recorded hit couldn't be retrieved"
    return refund


async def get_refund(refund_id: str) -> Optional[Refund]:
    row = await db.fetchone(
        f"SELECT * FROM boltcards.refunds WHERE id = ?", (refund_id)
    )
    if not row:
        return None
    refund = dict(**row)
    return Refund.parse_obj(refund)


async def get_refunds(hits_ids: Union[str, List[str]]) -> List[Refund]:
    q = ",".join(["?"] * len(hits_ids))
    rows = await db.fetchall(
        f"SELECT * FROM boltcards.refunds WHERE hit_id IN ({q})", (*hits_ids,)
    )

    return [Refund(**row) for row in rows]
