import asyncio
import traceback
from typing import Callable, Coroutine

import httpx
from loguru import logger

from lnbits.core.crud import (
    create_audit_entry,
    get_wallet,
    get_webpush_subscriptions_for_user,
    mark_webhook_sent,
)
from lnbits.core.crud.audit import delete_expired_audit_entries
from lnbits.core.crud.payments import get_payments_status_count
from lnbits.core.crud.users import get_accounts
from lnbits.core.crud.wallets import get_wallets_count
from lnbits.core.models import AuditEntry, Payment
from lnbits.core.models.extensions import InstallableExtension
from lnbits.core.models.notifications import NotificationType
from lnbits.core.services import (
    send_payment_notification,
)
from lnbits.core.services.funding_source import (
    check_balance_delta_changed,
    check_server_balance_against_node,
    get_balance_delta,
)
from lnbits.core.services.notifications import (
    enqueue_notification,
    process_next_notification,
)
from lnbits.db import Filters
from lnbits.helpers import check_callback_url
from lnbits.settings import settings
from lnbits.tasks import create_unique_task, send_push_notification
from lnbits.utils.exchange_rates import btc_rates

audit_queue: asyncio.Queue = asyncio.Queue()


async def run_by_the_minute_tasks():
    minute_counter = 0
    while settings.lnbits_running:
        status_minutes = settings.lnbits_notification_server_status_hours * 60

        if settings.notification_balance_delta_changed:
            try:
                # runs by default every minute, the delta should not change that often
                await check_balance_delta_changed()
            except Exception as ex:
                logger.error(ex)

        if minute_counter % settings.lnbits_watchdog_interval_minutes == 0:
            try:
                await check_server_balance_against_node()
            except Exception as ex:
                logger.error(ex)

        if minute_counter % status_minutes == 0:
            try:
                await _notify_server_status()
            except Exception as ex:
                logger.error(ex)

        if minute_counter % 60 == 0:
            try:
                # initialize the list of all extensions
                await InstallableExtension.get_installable_extensions()
            except Exception as ex:
                logger.error(ex)

        minute_counter += 1
        await asyncio.sleep(60)


async def _notify_server_status():
    accounts = await get_accounts(filters=Filters(limit=0))
    wallets_count = await get_wallets_count()
    payments = await get_payments_status_count()

    status = await get_balance_delta()
    values = {
        "up_time": settings.lnbits_server_up_time,
        "accounts_count": accounts.total,
        "wallets_count": wallets_count,
        "in_payments_count": payments.incoming,
        "out_payments_count": payments.outgoing,
        "pending_payments_count": payments.pending,
        "failed_payments_count": payments.failed,
        "delta_sats": status.delta_sats,
        "lnbits_balance_sats": status.lnbits_balance_sats,
        "node_balance_sats": status.node_balance_sats,
    }
    enqueue_notification(NotificationType.server_status, values)


async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
    """
    This worker dispatches events to all extensions and dispatches webhooks.
    """
    while settings.lnbits_running:
        payment = await invoice_paid_queue.get()
        logger.trace("received invoice paid event")
        # payment notification
        wallet = await get_wallet(payment.wallet_id)
        if wallet:
            await send_payment_notification(wallet, payment)
        # dispatch webhook
        if payment.webhook and not payment.webhook_status:
            await dispatch_webhook(payment)
        # dispatch push notification
        await send_payment_push_notification(payment)


async def dispatch_webhook(payment: Payment):
    """
    Dispatches the webhook to the webhook url.
    """
    logger.debug("sending webhook", payment.webhook)

    if not payment.webhook:
        return await mark_webhook_sent(payment.payment_hash, -1)

    headers = {"User-Agent": settings.user_agent}
    async with httpx.AsyncClient(headers=headers) as client:
        data = payment.dict()
        try:
            check_callback_url(payment.webhook)
            r = await client.post(payment.webhook, json=data, timeout=40)
            r.raise_for_status()
            await mark_webhook_sent(payment.payment_hash, r.status_code)
        except httpx.HTTPStatusError as exc:
            await mark_webhook_sent(payment.payment_hash, exc.response.status_code)
            logger.warning(
                f"webhook returned a bad status_code: {exc.response.status_code} "
                f"while requesting {exc.request.url!r}."
            )
        except httpx.RequestError:
            await mark_webhook_sent(payment.payment_hash, -1)
            logger.warning(f"Could not send webhook to {payment.webhook}")


async def send_payment_push_notification(payment: Payment):
    wallet = await get_wallet(payment.wallet_id)

    if wallet:
        subscriptions = await get_webpush_subscriptions_for_user(wallet.user)

        amount = int(payment.amount / 1000)

        title = f"LNbits: {wallet.name}"
        body = f"You just received {amount} sat{'s'[:amount^1]}!"

        if payment.memo:
            body += f"\r\n{payment.memo}"

        for subscription in subscriptions:
            # todo: review permissions when user-id-only not allowed
            # todo: replace all this logic with websockets?
            url = (
                f"https://{subscription.host}/wallet?usr={wallet.user}&wal={wallet.id}"
            )
            await send_push_notification(subscription, title, body, url)


async def wait_for_audit_data():
    """
    Waits for audit entries to be pushed to the queue.
    Then it inserts the entries into the DB.
    """
    while settings.lnbits_running:
        data: AuditEntry = await audit_queue.get()
        try:
            await create_audit_entry(data)
        except Exception as ex:
            logger.warning(ex)
            await asyncio.sleep(3)


async def wait_notification_messages():

    while settings.lnbits_running:
        try:
            await process_next_notification()
        except Exception as ex:
            logger.log("error", ex)
            await asyncio.sleep(3)


async def purge_audit_data():
    """
    Remove audit entries which have passed their retention period.
    """
    while settings.lnbits_running:
        try:
            await delete_expired_audit_entries()
        except Exception as ex:
            logger.warning(ex)

        # clean every hour
        await asyncio.sleep(60 * 60)


async def collect_exchange_rates_data():
    """
    Collect exchange rates data. Used for monitoring only.
    """
    while settings.lnbits_running:
        currency = settings.lnbits_default_accounting_currency or "USD"
        max_history_size = settings.lnbits_exchange_history_size
        sleep_time = settings.lnbits_exchange_history_refresh_interval_seconds

        if sleep_time > 0:
            try:
                rates = await btc_rates(currency)
                if rates:
                    rates_values = [r[1] for r in rates]
                    lnbits_rate = sum(rates_values) / len(rates_values)
                    rates.append(("LNbits", lnbits_rate))
                settings.append_exchange_rate_datapoint(dict(rates), max_history_size)
            except Exception as ex:
                logger.warning(ex)
        else:
            sleep_time = 60
        await asyncio.sleep(sleep_time)


def _create_unique_task(name: str, func: Callable):
    async def _to_coro(func: Callable[[], Coroutine]) -> Coroutine:
        return await func()

    try:
        create_unique_task(name, _to_coro(func))
    except Exception as e:
        logger.error(f"Error in {name} task", e)
        logger.error(traceback.format_exc())