import hashlib
import json
from django.conf import settings
from django.utils import timezone
from decimal import Decimal
import uuid
from django.core.exceptions import ValidationError
from django.db import IntegrityError, transaction
from .models import Wallet, LedgerTransaction, LedgerEntry, Transfer, Withdrawal, IdempotencyKey


def minor_units(asset, raw):
    try:
        value = str(raw).strip()
        if not value or value.startswith("-"):
            raise ValueError
        whole, dot, frac = value.partition(".")
        precision = 0 if asset == "TZS" else 2
        if dot and (len(frac) > precision or not frac.isdigit()):
            raise ValueError
        if not whole.isdigit() or (dot and not frac):
            raise ValueError
        return int(whole) * (10 ** precision) + (int(frac.ljust(precision, "0")) if precision else 0)
    except (ValueError, TypeError):
        raise ValidationError("Enter a valid positive amount with supported precision.")


def display_amount(asset, amount):
    if asset == "TZS":
        return f"{amount:,}"
    return f"{amount // 100}.{amount % 100:02d}"


def _request_hash(payload):
    canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"), default=str)
    return hashlib.sha256(canonical.encode()).hexdigest()


@transaction.atomic
def post_transfer(sender, recipient, asset, amount_minor, *, idempotency_key=None, memo="internal transfer"):
    if sender.pk == recipient.pk:
        raise ValidationError("You cannot send money to yourself.")
    if amount_minor <= 0:
        raise ValidationError("Amount must be greater than zero.")
    request_hash = _request_hash({"recipient": recipient.pk, "asset": asset, "amount_minor": amount_minor})

    if idempotency_key:
        existing = IdempotencyKey.objects.select_for_update().filter(user=sender, key=idempotency_key).first()
        if existing:
            if existing.request_hash != request_hash:
                raise ValidationError("This idempotency key was already used for a different request.")
            if existing.transaction_id:
                return existing.transaction
            raise ValidationError("The previous request is still being processed.")

    sender_wallet = Wallet.objects.select_for_update().get(user=sender, asset=asset)
    recipient_wallet, _ = Wallet.objects.get_or_create(user=recipient, asset=asset)
    recipient_wallet = Wallet.objects.select_for_update().get(pk=recipient_wallet.pk)
    if sender_wallet.balance_minor < amount_minor:
        raise ValidationError("Insufficient available balance.")

    sender_wallet.balance_minor -= amount_minor
    recipient_wallet.balance_minor += amount_minor
    sender_wallet.save(update_fields=["balance_minor"])
    recipient_wallet.save(update_fields=["balance_minor"])

    tx = LedgerTransaction.objects.create(kind="transfer", memo=memo[:200])
    LedgerEntry.objects.bulk_create([
        LedgerEntry(transaction=tx, wallet=sender_wallet, direction=LedgerEntry.DEBIT, amount_minor=amount_minor),
        LedgerEntry(transaction=tx, wallet=recipient_wallet, direction=LedgerEntry.CREDIT, amount_minor=amount_minor),
    ])
    Transfer.objects.create(transaction=tx, sender=sender, recipient=recipient, asset=asset,
                            amount_minor=amount_minor, idempotency_key=idempotency_key or "")
    if idempotency_key:
        try:
            IdempotencyKey.objects.create(user=sender, key=idempotency_key, request_hash=request_hash, transaction=tx)
        except IntegrityError:
            # A concurrent request can only safely reuse the already committed transaction.
            existing = IdempotencyKey.objects.get(user=sender, key=idempotency_key)
            if existing.request_hash != request_hash or not existing.transaction_id:
                raise ValidationError("The transfer request could not be safely deduplicated.")
            return existing.transaction
    return tx


@transaction.atomic
def simulate_withdrawal(user, asset, amount_minor, channel, destination, network=""):
    """Local/demo withdrawal only. Production payouts must use an authorized provider.

    This function is intentionally retained for the local UI so the upgraded project
    starts cleanly and existing demo workflows continue to work. It is disabled when
    ALLOW_TEST_FUNDS is false.
    """
    if not settings.ALLOW_TEST_FUNDS:
        raise ValidationError("Test withdrawals are disabled. Use a configured payment provider.")
    if amount_minor <= 0:
        raise ValidationError("Amount must be greater than zero.")
    if channel not in {Withdrawal.BANK, Withdrawal.BLOCKCHAIN}:
        raise ValidationError("Unsupported withdrawal channel.")
    if not destination or not str(destination).strip():
        raise ValidationError("A withdrawal destination is required.")
    if channel == Withdrawal.BLOCKCHAIN and asset != Wallet.USDT:
        raise ValidationError("Blockchain demo withdrawals currently support USDT only.")

    wallet = Wallet.objects.select_for_update().get(user=user, asset=asset)
    if wallet.balance_minor < amount_minor:
        raise ValidationError("Insufficient available balance.")

    wallet.balance_minor -= amount_minor
    wallet.save(update_fields=["balance_minor"])

    rate = Decimal("2500.00") if asset == Wallet.USDT and channel == Withdrawal.BANK else None
    fiat_amount_tzs = amount_minor if asset == Wallet.TZS and channel == Withdrawal.BANK else 0
    if rate is not None:
        fiat_amount_tzs = int((Decimal(amount_minor) / Decimal("100")) * rate)

    withdrawal = Withdrawal.objects.create(
        user=user,
        wallet=wallet,
        channel=channel,
        destination=str(destination).strip(),
        amount_minor=amount_minor,
        network=str(network or ""),
        exchange_rate_tzs_per_usdt=rate,
        fiat_amount_tzs=fiat_amount_tzs,
        payout_reference=f"SIM-PAYOUT-{uuid.uuid4().hex[:16].upper()}",
        status=Withdrawal.COMPLETED,
        completed_at=timezone.now(),
    )

    tx = LedgerTransaction.objects.create(
        kind="simulated_withdrawal",
        memo=f"TEST WITHDRAWAL {withdrawal.reference}",
    )
    LedgerEntry.objects.create(
        transaction=tx, wallet=wallet, direction=LedgerEntry.DEBIT, amount_minor=amount_minor
    )
    return withdrawal


@transaction.atomic
def simulate_deposit(user, asset, amount_minor):
    """Local/demo funding only. Production deposits must come from a provider webhook/API."""
    if not settings.ALLOW_TEST_FUNDS:
        raise ValidationError("Test deposits are disabled. Use a configured payment provider.")
    if amount_minor <= 0:
        raise ValidationError("Amount must be greater than zero.")
    wallet, _ = Wallet.objects.get_or_create(user=user, asset=asset)
    wallet = Wallet.objects.select_for_update().get(pk=wallet.pk)
    wallet.balance_minor += amount_minor
    wallet.save(update_fields=["balance_minor"])
    tx = LedgerTransaction.objects.create(kind="simulated_deposit", memo="TEST FUNDS ONLY")
    LedgerEntry.objects.create(transaction=tx, wallet=wallet, direction=LedgerEntry.CREDIT, amount_minor=amount_minor)
    return tx


def create_tron_usdt_withdrawal(user, amount_minor, destination, *, idempotency_key=""):
    """Create and submit a TRON Shasta USDT withdrawal.

    The internal ledger reservation is committed before the external broadcast.
    A failed broadcast refunds the reservation. A submitted transaction remains
    PROCESSING until a later refresh confirms it on-chain.
    """
    if amount_minor <= 0:
        raise ValidationError("Amount must be greater than zero.")
    if not idempotency_key:
        raise ValidationError("An idempotency key is required for blockchain withdrawals.")
    destination = destination.strip()
    request_hash = _request_hash({
        "asset": "USDT", "amount_minor": amount_minor,
        "destination": destination, "network": "Tron-Shasta",
    })

    with transaction.atomic():
        existing = IdempotencyKey.objects.select_for_update().filter(user=user, key=idempotency_key).first()
        if existing:
            if existing.request_hash != request_hash:
                raise ValidationError("This idempotency key was already used for a different request.")
            if existing.transaction_id:
                withdrawal = Withdrawal.objects.filter(user=user, idempotency_key=idempotency_key).first()
                if withdrawal:
                    return withdrawal
            raise ValidationError("The previous request is still being processed.")

        wallet = Wallet.objects.select_for_update().get(user=user, asset=Wallet.USDT)
        if wallet.balance_minor < amount_minor:
            raise ValidationError("Insufficient available USDT balance.")

        withdrawal = Withdrawal.objects.create(
            user=user, wallet=wallet, channel=Withdrawal.BLOCKCHAIN,
            destination=destination, amount_minor=amount_minor,
            network="Tron-Shasta", status=Withdrawal.PROCESSING,
            idempotency_key=idempotency_key,
        )
        tx = LedgerTransaction.objects.create(
            kind="tron_usdt_withdrawal",
            memo=f"TRON TESTNET {withdrawal.reference}",
        )
        LedgerEntry.objects.create(
            transaction=tx, wallet=wallet, direction=LedgerEntry.DEBIT, amount_minor=amount_minor
        )
        wallet.balance_minor -= amount_minor
        wallet.save(update_fields=["balance_minor"])
        withdrawal.payout_reference = f"TRON-SHASTA-{withdrawal.reference}"
        withdrawal.save(update_fields=["payout_reference", "updated_at"])
        IdempotencyKey.objects.create(
            user=user, key=idempotency_key, request_hash=request_hash, transaction=tx
        )

    try:
        from .tron_service import send_usdt_testnet
        result = send_usdt_testnet(destination, amount_minor)
    except ValidationError as exc:
        with transaction.atomic():
            locked = Withdrawal.objects.select_for_update().get(pk=withdrawal.pk)
            if locked.status == Withdrawal.PROCESSING and not locked.blockchain_tx_hash:
                wallet = Wallet.objects.select_for_update().get(pk=locked.wallet_id)
                wallet.balance_minor += amount_minor
                wallet.save(update_fields=["balance_minor"])
                locked.status = Withdrawal.FAILED
                locked.failure_reason = str(exc)[:250]
                locked.save(update_fields=["status", "failure_reason", "updated_at"])
        raise

    with transaction.atomic():
        locked = Withdrawal.objects.select_for_update().get(pk=withdrawal.pk)
        locked.blockchain_tx_hash = result["txid"]
        locked.blockchain_status = "SUBMITTED"
        locked.blockchain_submitted_at = result["submitted_at"]
        locked.provider_reference = result["txid"]
        locked.save(update_fields=[
            "blockchain_tx_hash", "blockchain_status", "blockchain_submitted_at",
            "provider_reference", "updated_at",
        ])
        return locked


def refresh_tron_withdrawal(withdrawal):
    """Refresh a submitted withdrawal from TRON Shasta."""
    if withdrawal.channel != Withdrawal.BLOCKCHAIN or withdrawal.network != "Tron-Shasta":
        raise ValidationError("This withdrawal is not a TRON Shasta withdrawal.")
    if not withdrawal.blockchain_tx_hash:
        raise ValidationError("No blockchain transaction hash is recorded.")
    from .tron_service import get_transaction_status
    status = get_transaction_status(withdrawal.blockchain_tx_hash)
    if not status["found"]:
        return withdrawal
    if status.get("result") not in {"", "SUCCESS"}:
        withdrawal.status = Withdrawal.FAILED
        withdrawal.blockchain_status = str(status.get("result"))[:40]
        withdrawal.failure_reason = "TRON transaction was not successful."
        withdrawal.save(update_fields=["status", "blockchain_status", "failure_reason", "updated_at"])
        return withdrawal
    withdrawal.blockchain_confirmations = status.get("confirmations", 0)
    withdrawal.blockchain_status = "CONFIRMED" if status["confirmed"] else "CONFIRMING"
    if status["confirmed"] and withdrawal.status != Withdrawal.COMPLETED:
        withdrawal.status = Withdrawal.COMPLETED
        withdrawal.completed_at = timezone.now()
    withdrawal.save(update_fields=[
        "blockchain_confirmations", "blockchain_status", "status", "completed_at", "updated_at"
    ])
    return withdrawal
