package com.bitnix.dirtcryptostore.manager;

import com.bitnix.dirtcryptostore.DirtCryptoStorePlugin;
import com.bitnix.dirtcryptostore.model.PendingInvoice;
import com.bitnix.dirtcryptostore.model.Product;
import com.bitnix.dirtcryptostore.model.WalletConfig;
import com.bitnix.dirtcryptostore.storage.SQLiteStorage;
import org.bukkit.entity.Player;

import java.util.*;
import java.util.concurrent.ThreadLocalRandom;

public class InvoiceManager {

    private final DirtCryptoStorePlugin plugin;
    private final SQLiteStorage storage;

    private final Map<UUID, PendingInvoice> pendingByPlayer = new HashMap<>();
    private final Map<String, PendingInvoice> allByInvoiceId = new HashMap<>();
    private final Map<String, String> invoiceIdByTxid = new HashMap<>();
    private final Set<Long> reservedActiveAmounts = new HashSet<>();

    private long sequentialCounter = 1L;

    public InvoiceManager(DirtCryptoStorePlugin plugin) {
        this.plugin = plugin;
        this.storage = new SQLiteStorage(plugin);
    }

    public synchronized void init() {
        storage.init();
    }

    public synchronized void load() {
        clearAllInternal(false);

        List<PendingInvoice> invoices = storage.loadAllInvoices();
        for (PendingInvoice invoice : invoices) {
            allByInvoiceId.put(invoice.getInvoiceId(), invoice);

            if (!invoice.isFinalState()) {
                pendingByPlayer.put(invoice.getPlayerUuid(), invoice);
                reservedActiveAmounts.add(invoice.getAmountSats());
            }

            if (!invoice.getTxid().isEmpty()) {
                invoiceIdByTxid.put(invoice.getTxid(), invoice.getInvoiceId());
            }
        }

        pruneExpiredAndOld();
    }

    public synchronized void save() {
        for (PendingInvoice invoice : allByInvoiceId.values()) {
            storage.saveInvoice(invoice);
        }
    }

    public synchronized PendingInvoice getPendingInvoice(UUID uuid) {
        PendingInvoice invoice = pendingByPlayer.get(uuid);
        if (invoice == null) {
            return null;
        }

        if (invoice.getStatus() == PendingInvoice.Status.PENDING && invoice.isExpired()) {
            expireInvoice(invoice, "Expired while checking pending invoice.");
            return null;
        }

        return invoice.isFinalState() ? null : invoice;
    }

    public synchronized PendingInvoice getInvoiceById(String invoiceId) {
        return allByInvoiceId.get(invoiceId);
    }

    public synchronized boolean isTxidAlreadyClaimed(String txid, String invoiceId) {
        String existing = invoiceIdByTxid.get(txid);
        return existing != null && !existing.equalsIgnoreCase(invoiceId);
    }

    public synchronized PendingInvoice createInvoice(Player player, Product product, WalletConfig wallet) {
        PendingInvoice existing = getPendingInvoice(player.getUniqueId());
        if (existing != null) {
            return null;
        }

        long resolvedBaseSats = plugin.getPricingManager().resolveBaseSats(product);
        if (resolvedBaseSats <= 0L) {
            return null;
        }

        long amountSats = generateUniqueAmount(resolvedBaseSats);
        long now = System.currentTimeMillis();
        long expireMinutes = plugin.getConfig().getLong("settings.payment.invoice-expire-minutes", 30L);
        long expiresAt = now + (expireMinutes * 60_000L);

        PendingInvoice invoice = new PendingInvoice(
                UUID.randomUUID().toString(),
                player.getUniqueId(),
                player.getName(),
                product.getId(),
                wallet.getId(),
                wallet.getAddress(),
                amountSats,
                now,
                expiresAt,
                PendingInvoice.Status.PENDING,
                "",
                0,
                false,
                0L,
                0L,
                "Invoice created."
        );

        pendingByPlayer.put(player.getUniqueId(), invoice);
        allByInvoiceId.put(invoice.getInvoiceId(), invoice);
        reservedActiveAmounts.add(amountSats);
        storage.saveInvoice(invoice);
        return invoice;
    }

    public synchronized void cancelInvoice(UUID uuid) {
        PendingInvoice invoice = pendingByPlayer.get(uuid);
        if (invoice == null || invoice.isFinalState()) {
            return;
        }

        invoice.setStatus(PendingInvoice.Status.CANCELLED);
        invoice.setNotes("Cancelled by player.");
        reservedActiveAmounts.remove(invoice.getAmountSats());
        pendingByPlayer.remove(uuid);
        storage.saveInvoice(invoice);
    }

    public synchronized void expireInvoice(PendingInvoice invoice, String note) {
        if (invoice == null || invoice.isFinalState()) {
            return;
        }

        invoice.setStatus(PendingInvoice.Status.EXPIRED);
        invoice.setNotes(note == null ? "Expired." : note);
        reservedActiveAmounts.remove(invoice.getAmountSats());
        pendingByPlayer.remove(invoice.getPlayerUuid());
        storage.saveInvoice(invoice);
    }

    public synchronized void markDetected(PendingInvoice invoice, String txid, int confirmations, String note) {
        if (invoice == null || invoice.isFinalState()) {
            return;
        }

        invoice.setStatus(PendingInvoice.Status.DETECTED);
        invoice.setTxid(txid);
        invoice.setConfirmations(confirmations);
        if (invoice.getMatchedAt() <= 0L) {
            invoice.setMatchedAt(System.currentTimeMillis());
        }
        invoice.setNotes(note == null ? "Payment detected." : note);

        if (txid != null && !txid.isEmpty()) {
            invoiceIdByTxid.put(txid, invoice.getInvoiceId());
        }

        storage.saveInvoice(invoice);
    }

    public synchronized void markFulfilled(PendingInvoice invoice, String txid, int confirmations, String note) {
        if (invoice == null || invoice.isFulfilled()) {
            return;
        }

        invoice.setStatus(PendingInvoice.Status.FULFILLED);
        invoice.setTxid(txid);
        invoice.setConfirmations(confirmations);
        invoice.setFulfilled(true);
        if (invoice.getMatchedAt() <= 0L) {
            invoice.setMatchedAt(System.currentTimeMillis());
        }
        invoice.setFulfilledAt(System.currentTimeMillis());
        invoice.setNotes(note == null ? "Payment fulfilled." : note);

        if (txid != null && !txid.isEmpty()) {
            invoiceIdByTxid.put(txid, invoice.getInvoiceId());
        }

        reservedActiveAmounts.remove(invoice.getAmountSats());
        pendingByPlayer.remove(invoice.getPlayerUuid());
        storage.saveInvoice(invoice);
    }

    public synchronized void markUnderReview(PendingInvoice invoice, String txid, int confirmations, String note) {
        if (invoice == null || invoice.isFinalState()) {
            return;
        }

        invoice.setStatus(PendingInvoice.Status.UNDER_REVIEW);
        invoice.setTxid(txid == null ? "" : txid);
        invoice.setConfirmations(confirmations);
        if (invoice.getMatchedAt() <= 0L && txid != null && !txid.isEmpty()) {
            invoice.setMatchedAt(System.currentTimeMillis());
        }
        invoice.setNotes(note == null ? "Marked under review." : note);

        if (txid != null && !txid.isEmpty()) {
            invoiceIdByTxid.put(txid, invoice.getInvoiceId());
        }

        reservedActiveAmounts.remove(invoice.getAmountSats());
        pendingByPlayer.remove(invoice.getPlayerUuid());
        storage.saveInvoice(invoice);
    }

    public synchronized Collection<PendingInvoice> getAllInvoices() {
        return new ArrayList<>(allByInvoiceId.values());
    }

    public synchronized void cleanupExpiredInvoices() {
        for (PendingInvoice invoice : new ArrayList<>(allByInvoiceId.values())) {
            if (invoice.getStatus() == PendingInvoice.Status.PENDING && invoice.isExpired()) {
                expireInvoice(invoice, "Expired by cleanup task.");
            }
        }
    }

    public synchronized void pruneExpiredAndOld() {
        cleanupExpiredInvoices();

        if (!plugin.getConfig().getBoolean("settings.storage.prune-enabled", true)) {
            return;
        }

        int pruneAfterDays = plugin.getConfig().getInt("settings.storage.prune-after-days", 30);
        long cutoff = System.currentTimeMillis() - (pruneAfterDays * 24L * 60L * 60L * 1000L);

        List<String> statuses = plugin.getConfig().getStringList("settings.storage.prune-statuses");
        if (statuses.isEmpty()) {
            return;
        }

        List<String> removeIds = new ArrayList<>();
        for (PendingInvoice invoice : allByInvoiceId.values()) {
            if (invoice.getCreatedAt() >= cutoff) {
                continue;
            }

            if (statuses.contains(invoice.getStatus().name())) {
                removeIds.add(invoice.getInvoiceId());
            }
        }

        for (String invoiceId : removeIds) {
            PendingInvoice removed = allByInvoiceId.remove(invoiceId);
            if (removed != null) {
                pendingByPlayer.remove(removed.getPlayerUuid());
                reservedActiveAmounts.remove(removed.getAmountSats());
                if (!removed.getTxid().isEmpty()) {
                    invoiceIdByTxid.remove(removed.getTxid());
                }
                storage.deleteInvoice(invoiceId);
            }
        }

        storage.pruneOldInvoices(cutoff, statuses);
    }

    public synchronized void clearAll() {
        clearAllInternal(true);
    }

    private void clearAllInternal(boolean resetCounter) {
        pendingByPlayer.clear();
        allByInvoiceId.clear();
        invoiceIdByTxid.clear();
        reservedActiveAmounts.clear();
        if (resetCounter) {
            sequentialCounter = 1L;
        }
    }

    private long generateUniqueAmount(long baseSats) {
        String mode = plugin.getConfig().getString("settings.micro-amount.mode", "RANDOM_RANGE");

        if ("SEQUENTIAL".equalsIgnoreCase(mode)) {
            long candidate = baseSats + Math.max(1L, sequentialCounter++);
            while (reservedActiveAmounts.contains(candidate)) {
                candidate++;
            }
            return candidate;
        }

        long min = plugin.getConfig().getLong("settings.micro-amount.min-extra-sats", 1L);
        long max = plugin.getConfig().getLong("settings.micro-amount.max-extra-sats", 200L);
        long attempts = plugin.getConfig().getLong("settings.micro-amount.max-generation-attempts", 500L);

        for (int i = 0; i < attempts; i++) {
            long randomExtra = ThreadLocalRandom.current().nextLong(min, max + 1);
            long candidate = baseSats + randomExtra;
            if (!reservedActiveAmounts.contains(candidate)) {
                return candidate;
            }
        }

        long candidate = baseSats + max + 1L;
        while (reservedActiveAmounts.contains(candidate)) {
            candidate++;
        }
        return candidate;
    }
}
