import json
import uuid

from django.db.models import Q, Sum
from django.shortcuts import redirect, render
from django.utils import timezone
from django.utils.html import format_html
from django.utils.translation import gettext_lazy
from eveuniverse.models import EveType

from allianceauth.services.hooks import get_extension_logger

from buybackprogram.app_settings import (
    BUYBACKPROGRAM_DISALLOW_CONTRACTS_WITH_ANY_DISALLOWED_ITEMS,
    BUYBACKPROGRAM_PRICE_SOURCE_NAME,
    BUYBACKPROGRAM_TRACKING_PREFILL,
)
from buybackprogram.forms import CalculatorForm
from buybackprogram.helpers import (
    get_item_buy_value,
    get_item_prices,
    get_item_values,
    get_or_create_prices,
    get_tracking_number,
    item_missing,
)
from buybackprogram.models import (
    Program,
    ProgramItem,
    ReverseProgram,
    ReverseTracking,
    ReverseTrackingItem,
)
from buybackprogram.utils import messages_plus

logger = get_extension_logger(__name__)


def program_calculate(request, program_pk):
    program = Program.objects.filter(pk=program_pk).first()

    if program is None:
        return redirect("buybackprogram:index")

    buyback_data = []
    additional_notes = False

    form_donation = False

    if not program.is_public and not request.user.is_authenticated:
        return redirect("buybackprogram:index")

    if request.method != "POST":
        form = CalculatorForm()

    else:
        form = CalculatorForm(request.POST)

        if form.is_valid():
            form_items = form.cleaned_data["items"]
            form_donation = form.cleaned_data["donation"]
            additional_notes = form.cleaned_data[
                "additional_notes"
            ]  # Capture additional notes

            # If we have an ingame copy paste
            if "\t" in form_items:
                # Split items by rows
                for item in form_items.split("\n"):
                    item_accepted = True
                    notes = []

                    # get item name and quantity
                    parts = item.split("\t")

                    # Get item name from the first part
                    name = parts[0].replace("*", "")

                    item_type = EveType.objects.filter(name=name).first()

                    # Check if we have a match from the database for the item
                    if item_type:
                        item_category = item_type.eve_group.eve_category

                        program_item = ProgramItem.objects.filter(
                            program=program, item_type=item_type
                        ).first()
                        is_forbidden = (
                            not program.allow_all_items and not program_item
                        ) or (program_item and program_item.disallow_item)

                        if is_forbidden:
                            item_accepted = False
                            note = {
                                "icon": "fa-ban",
                                "color": "red",
                                "message": "%s is explicitly forbidden in this buyback program."
                                % name,
                            }
                            notes.append(note)

                        # Check if item is a blueprint
                        if item_category.name == "Blueprint":
                            item_accepted = False
                            note = {
                                "icon": "fa-print",
                                "color": "orange",
                                "message": "%s belongs to category %s. Blueprints are not accepted."
                                % (name, item_category),
                            }
                            notes.append(note)
                    else:
                        item_category = False
                        item_accepted = False
                        note = {
                            "icon": "fa-skull-crossbones",
                            "color": "red",
                            "message": "%s not found from database. It is most likely a new item still not added to database or a renamed unpacked item."
                            % name,
                        }
                        notes.append(note)

                    # Anything else
                    if len(parts) == 1:
                        if program.allow_unpacked_items:
                            quantity = 1

                        else:
                            quantity = 1
                            item_accepted = False

                            note = {
                                "icon": "fa-box-open",
                                "color": "red",
                                "message": "Unpacked items are now allowed at this location. Repack %s to get a price for it"
                                % name,
                            }

                            notes.append(note)

                    # Icons view
                    elif len(parts) == 2:
                        # Get item quantity.
                        if not parts[1] == "\r":
                            # Get quantities and format the different localization imputs
                            quantity = int("".join(filter(str.isdigit, parts[1])))

                        elif program.allow_unpacked_items:
                            quantity = 1

                        else:
                            quantity = 1
                            item_accepted = False

                            note = {
                                "icon": "fa-box-open",
                                "color": "red",
                                "message": "Unpacked items are now allowed at this location. Repack %s to get a price for it"
                                % name,
                            }

                            notes.append(note)

                    # Detail view
                    else:
                        # Get item quantity.
                        if parts[1]:
                            # Get quantities and format the different localization imputs
                            quantity = int("".join(filter(str.isdigit, parts[1])))

                        elif program.allow_unpacked_items:
                            quantity = 1

                        else:
                            quantity = 1
                            item_accepted = False

                            note = {
                                "icon": "fa-box-open",
                                "color": "red",
                                "message": "Unpacked items are now allowed at this location. Repack %s to get a price for it"
                                % name,
                            }

                            notes.append(note)

                    # Get details for the item
                    if item_accepted:
                        # Get item material, compression and price information
                        item_prices = get_item_prices(
                            item_type,
                            name,
                            quantity,
                            program,
                        )

                        # Get item values with taxes
                        item_values = get_item_values(item_type, item_prices, program)

                        # Final form of the built buyback item that will be pushed to the item array
                        buyback_item = {
                            "type_data": item_type,
                            "item_prices": item_prices,
                            "item_values": item_values,
                        }

                        # Append buyback item data to the total array
                        buyback_data.append(buyback_item)

                    # If items are not accepted for some reason
                    else:
                        item_values = item_missing(name, quantity)

                        buyback_item = {
                            "type_data": item_type,
                            "item_prices": {
                                "notes": notes,
                                "raw_prices": False,
                                "material_prices": False,
                                "compression_prices": False,
                                "npc_prices": False,
                            },
                            "item_values": item_values,
                        }

                        buyback_data.append(buyback_item)

            else:
                messages_plus.error(
                    request,
                    format_html(
                        gettext_lazy(
                            "Buyback calculator only accepts copy pasted item formats from ingame. To calculate a price copy the items from your inventory."
                        )
                    ),
                )

                logger.debug("TODO: add tasks to process plain text imputs here.")

    if BUYBACKPROGRAM_DISALLOW_CONTRACTS_WITH_ANY_DISALLOWED_ITEMS:
        contract_has_errors = any(
            item["item_prices"].get("raw_prices") is False for item in buyback_data
        )

        if contract_has_errors:
            messages_plus.error(
                request,
                "Calculation Failed - Prohibited or unrecognized items detected. Please remove the highlighted items and try again.",
            )

            context = {
                "program": program,
                "form": form,
                "buyback_data": buyback_data,
                "tracking_number": None,  # Force no tracking number
                "price_source": BUYBACKPROGRAM_PRICE_SOURCE_NAME,
            }
            return render(request, "buybackprogram/program_calculate.html", context)

    # Get item values after other expenses and the total value for the contract
    contract_price_data = get_item_buy_value(buyback_data, program, form_donation)

    logger.debug(
        "Calculated contract net total is %s"
        % contract_price_data["contract_net_total"]
    )

    # Get item values after other expenses and the total value for the contract
    user = request.user if request.user.is_authenticated else None
    tracking = get_tracking_number(
        user,
        program,
        form_donation,
        buyback_data,
        contract_price_data,
        additional_notes,
    )

    tracking_number = tracking.tracking_number

    context = {
        "program": program,
        "form": form,
        "donation": form_donation,
        "buyback_data": buyback_data,
        "contract_price_data": contract_price_data,
        "tracking_number": tracking_number,
        "price_source": BUYBACKPROGRAM_PRICE_SOURCE_NAME,
    }

    return render(request, "buybackprogram/program_calculate.html", context)


def reverse_program_calculate(request, program_pk):
    program = ReverseProgram.objects.filter(pk=program_pk).first()

    if program is None:
        return redirect("buybackprogram:reverse_index")

    if not program.is_public and not request.user.is_authenticated:
        return redirect("buybackprogram:reverse_index")

    # Build stock list with prices for display
    stock = program.stock.select_related("eve_type").all()

    stock_data = []
    for item in stock:
        prices = get_or_create_prices(item.eve_type_id)
        if prices:
            if program.price_type == "Buy":
                base_price = float(prices.buy)
            elif program.price_type == "Sell":
                base_price = float(prices.sell)
            else:  # Split
                base_price = (float(prices.buy) + float(prices.sell)) / 2
        else:
            base_price = 0

        unit_price = base_price * (1 + program.markup / 100)

        stock_data.append(
            {
                "type_id": item.eve_type_id,
                "name": item.eve_type.name,
                "group": item.eve_type.eve_group.name
                if item.eve_type.eve_group
                else "",
                "category": item.eve_type.eve_group.eve_category.name
                if item.eve_type.eve_group and item.eve_type.eve_group.eve_category
                else "",
                "available": item.quantity,
                "unit_price": round(unit_price, 2),
            }
        )

    # Compute reserved quantities from outstanding contracts and unlinked requests
    reserved_map = {}
    reserved_items = (
        ReverseTrackingItem.objects.filter(
            tracking__program=program,
        )
        .filter(
            Q(tracking__contract__status="outstanding")
            | Q(tracking__contract__isnull=True)
        )
        .values("eve_type_id")
        .annotate(reserved=Sum("quantity"))
    )
    for r in reserved_items:
        reserved_map[r["eve_type_id"]] = r["reserved"]

    # Apply reserved quantities to stock_data
    for item_data in stock_data:
        reserved = reserved_map.get(item_data["type_id"], 0)
        item_data["reserved"] = reserved
        item_data["available_display"] = max(0, item_data["available"] - reserved)

    selected_items = []
    contract_net_total = 0
    tracking_number = None

    if request.method == "POST":
        raw = request.POST.get("selected_items", "[]")
        try:
            selections = json.loads(raw)
        except json.JSONDecodeError:
            selections = []

        stock_map = {s["type_id"]: s for s in stock_data}

        for sel in selections:
            type_id = int(sel.get("type_id", 0))
            quantity = int(sel.get("quantity", 0))

            if type_id not in stock_map or quantity <= 0:
                continue

            item_data = stock_map[type_id]
            available = item_data["available_display"]  # available after reservations
            unit_price = item_data["unit_price"]
            total_price = unit_price * quantity

            notes = []
            if quantity > available:
                notes.append(
                    {
                        "icon": "fa-exclamation-triangle",
                        "color": "orange",
                        "message": "Requested %s but only %s available after reservations."
                        % (quantity, available),
                    }
                )
                quantity = available
                total_price = unit_price * quantity

            contract_net_total += total_price

            selected_items.append(
                {
                    "type_id": type_id,
                    "name": item_data["name"],
                    "quantity": quantity,
                    "unit_price": unit_price,
                    "total_price": total_price,
                    "available": available,
                    "notes": notes,
                }
            )

        if selected_items:
            user = request.user if request.user.is_authenticated else None

            try:
                last_id = ReverseTracking.objects.last().id
            except AttributeError:
                last_id = 0

            tracking_number_str = (
                BUYBACKPROGRAM_TRACKING_PREFILL
                + "-R-"
                + str(last_id)
                + "-"
                + uuid.uuid4().hex[:6].upper()
            )

            tracking = ReverseTracking(
                program=program,
                issuer_user=user,
                net_price=contract_net_total,
                tracking_number=tracking_number_str,
                created_at=timezone.now(),
            )
            tracking.save()

            objs = [
                ReverseTrackingItem(
                    tracking=tracking,
                    eve_type_id=item["type_id"],
                    quantity=item["quantity"],
                    buy_value=item["unit_price"],
                )
                for item in selected_items
            ]
            ReverseTrackingItem.objects.bulk_create(objs)
            tracking_number = tracking_number_str

    context = {
        "program": program,
        "stock_data_json": json.dumps(stock_data),
        "stock_data": stock_data,
        "selected_items": selected_items,
        "contract_net_total": contract_net_total,
        "tracking_number": tracking_number,
        "is_reverse_section": True,
    }

    return render(
        request, "buybackprogram/reverse_program/reverse_calculate.html", context
    )
