"""
inventory/admin_reports.py
───────────────────────────
"Reports & Dashboard" admin section. Hangs off the unmanaged ReportsHub
proxy model (see inventory/models.py) — no table, no add/delete, just a
nav entry and a set of custom admin views hosted via get_urls().

Permission mapping (see inventory/permissions_admin.py):
  Dashboard, Sales, Books        — inventory_managers OR finance_managers
  Income/P&L, Author statements  — finance_managers
  Inventory/Stock                — inventory_managers
"""

from datetime import date

from django import forms
from django.contrib import admin, messages
from django.contrib.admin.widgets import AutocompleteSelect
from django.core.exceptions import PermissionDenied
from django.shortcuts import redirect, render
from django.urls import path, reverse
from django.utils import timezone
from django.utils.http import urlencode

from catalog_app.models import Author, Book, Publisher
from inventory.models import AuthorPayout, ReportsHub, Sale, SaleItem, StockLocation
from inventory.permissions_admin import (
    is_finance_manager,
    is_inventory_manager,
    is_inventory_or_finance_manager,
)
from inventory.services import export_service, pdf_service, payout_service, report_service
from inventory.services.author_statement_service import send_author_statement_email


def _parse_date_range(request, month_to_date: bool = True):
    today = date.today()
    start_str = request.GET.get("start_date")
    end_str = request.GET.get("end_date")
    start = date.fromisoformat(start_str) if start_str else (today.replace(day=1) if month_to_date else today)
    end = date.fromisoformat(end_str) if end_str else today
    return start, end


def _default_granularity(start, end):
    """Pick a sensible revenue-over-time bucket size for the span — a
    13-day range bucketed by month collapses to a single point."""
    span_days = (end - start).days
    if span_days <= 31:
        return "day"
    if span_days <= 180:
        return "week"
    return "month"


def _author_autocomplete_widget():
    return AutocompleteSelect(
        AuthorPayout._meta.get_field("author"),
        admin.site,
        attrs={"data-placeholder": "Type to search authors…"},
    )


def _publisher_autocomplete_widget():
    return AutocompleteSelect(
        AuthorPayout._meta.get_field("publisher"),
        admin.site,
        attrs={"data-placeholder": "Type to search publishers…"},
    )


def _book_autocomplete_widget():
    return AutocompleteSelect(
        SaleItem._meta.get_field("book"),
        admin.site,
        attrs={"data-placeholder": "Type to search books…"},
    )


class AuthorReportFilterForm(forms.Form):
    author = forms.ModelChoiceField(
        queryset=Author.objects.order_by("name"),
        widget=_author_autocomplete_widget(),
        required=False,
        help_text="See everything owed to this author across all their books.",
    )
    publisher = forms.ModelChoiceField(
        queryset=Publisher.objects.order_by("name"),
        widget=_publisher_autocomplete_widget(),
        required=False,
        help_text="Or see everything owed to a publisher, across all their books.",
    )
    book = forms.ModelChoiceField(
        queryset=Book.objects.select_related("author", "publisher").order_by("title"),
        widget=_book_autocomplete_widget(),
        required=False,
        help_text="Or scope the statement to a single title.",
    )
    start_date = forms.DateField(required=False, widget=forms.DateInput(attrs={"type": "date"}))
    end_date = forms.DateField(required=False, widget=forms.DateInput(attrs={"type": "date"}))

    def clean(self):
        data = super().clean()
        if not data.get("author") and not data.get("publisher") and not data.get("book"):
            raise forms.ValidationError("Choose an author, a publisher, or a book title.")
        if data.get("author") and data.get("publisher"):
            raise forms.ValidationError("Choose either an author or a publisher, not both.")
        return data


@admin.register(ReportsHub)
class ReportsHubAdmin(admin.ModelAdmin):

    # ------------------------------------------------------------------
    # Visibility / permissions
    # ------------------------------------------------------------------

    def has_module_permission(self, request):
        return is_inventory_or_finance_manager(request.user)

    def has_view_permission(self, request, obj=None):
        return is_inventory_or_finance_manager(request.user)

    def has_add_permission(self, request):
        return False

    def has_change_permission(self, request, obj=None):
        return False

    def has_delete_permission(self, request, obj=None):
        return False

    def changelist_view(self, request, extra_context=None):
        return redirect("admin:inventory_reportshub_dashboard")

    # ------------------------------------------------------------------
    # URLs
    # ------------------------------------------------------------------

    def get_urls(self):
        custom = [
            path("dashboard/", self.admin_site.admin_view(self.dashboard_view), name="inventory_reportshub_dashboard"),

            path("pl/", self.admin_site.admin_view(self.pl_view), name="inventory_reportshub_pl"),
            path("pl/pdf/", self.admin_site.admin_view(self.pl_pdf_view), name="inventory_reportshub_pl_pdf"),
            path("pl/xlsx/", self.admin_site.admin_view(self.pl_xlsx_view), name="inventory_reportshub_pl_xlsx"),

            path("inventory/", self.admin_site.admin_view(self.inventory_view), name="inventory_reportshub_inventory"),
            path("inventory/pdf/", self.admin_site.admin_view(self.inventory_pdf_view), name="inventory_reportshub_inventory_pdf"),
            path("inventory/xlsx/", self.admin_site.admin_view(self.inventory_xlsx_view), name="inventory_reportshub_inventory_xlsx"),

            path("sales/", self.admin_site.admin_view(self.sales_view), name="inventory_reportshub_sales"),
            path("sales/xlsx/", self.admin_site.admin_view(self.sales_xlsx_view), name="inventory_reportshub_sales_xlsx"),

            path("books/", self.admin_site.admin_view(self.books_view), name="inventory_reportshub_books"),
            path("books/xlsx/", self.admin_site.admin_view(self.books_xlsx_view), name="inventory_reportshub_books_xlsx"),

            path("stock-received/", self.admin_site.admin_view(self.stock_received_view), name="inventory_reportshub_stock_received"),
            path("stock-received/xlsx/", self.admin_site.admin_view(self.stock_received_xlsx_view), name="inventory_reportshub_stock_received_xlsx"),
            path("stock-received/csv/", self.admin_site.admin_view(self.stock_received_csv_view), name="inventory_reportshub_stock_received_csv"),

            path("authors/", self.admin_site.admin_view(self.authors_view), name="inventory_reportshub_authors"),
            path("authors/pdf/", self.admin_site.admin_view(self.authors_pdf_view), name="inventory_reportshub_authors_pdf"),
            path("authors/xlsx/", self.admin_site.admin_view(self.authors_xlsx_view), name="inventory_reportshub_authors_xlsx"),
            path("authors/send/", self.admin_site.admin_view(self.authors_send_view), name="inventory_reportshub_authors_send"),
        ]
        return custom + super().get_urls()

    def _ctx(self, request, title, active, **extra):
        return {
            **self.admin_site.each_context(request),
            "title": title,
            "active": active,
            "opts": self.model._meta,
            "media": self.media,
            **extra,
        }

    # ------------------------------------------------------------------
    # Dashboard
    # ------------------------------------------------------------------

    def dashboard_view(self, request):
        if not is_inventory_or_finance_manager(request.user):
            raise PermissionDenied

        today = date.today()
        month_start = today.replace(day=1)

        pl = report_service.profit_and_loss(month_start, today)
        top_books = report_service.top_selling_books(month_start, today, limit=5)
        by_channel = report_service.sales_by_channel(month_start, today)
        pending_payouts = report_service.pending_payouts_by_author()
        revenue_trend = report_service.revenue_over_time(month_start, today, granularity="day")

        context = self._ctx(
            request, "Dashboard", "dashboard",
            period={"start": month_start, "end": today},
            pl=pl,
            top_books=top_books,
            by_channel=by_channel,
            pending_payouts=pending_payouts,
            revenue_trend=revenue_trend,
        )
        return render(request, "inventory/admin/dashboard.html", context)

    # ------------------------------------------------------------------
    # Income / P&L
    # ------------------------------------------------------------------

    def pl_view(self, request):
        if not is_finance_manager(request.user):
            raise PermissionDenied
        start, end = _parse_date_range(request)
        pl = report_service.profit_and_loss(start, end)
        expenses = report_service.expenses_by_category(start, end)
        by_channel = report_service.sales_by_channel(start, end)
        context = self._ctx(
            request, "Income / Profit & Loss", "pl",
            start=start, end=end, pl=pl,
            expenses_by_category=expenses, sales_by_channel=by_channel,
        )
        return render(request, "inventory/admin/reports_pl.html", context)

    def pl_pdf_view(self, request):
        if not is_finance_manager(request.user):
            raise PermissionDenied
        start, end = _parse_date_range(request)
        context = {
            "pl": report_service.profit_and_loss(start, end),
            "expenses_by_category": report_service.expenses_by_category(start, end),
            "sales_by_channel": report_service.sales_by_channel(start, end),
            "generated_at": timezone.now(),
        }
        return pdf_service.render_pdf("inventory/pdf/profit_loss.html", context, f"PL-{start}-{end}.pdf")

    def pl_xlsx_view(self, request):
        if not is_finance_manager(request.user):
            raise PermissionDenied
        start, end = _parse_date_range(request)
        pl = report_service.profit_and_loss(start, end)
        expenses = report_service.expenses_by_category(start, end)
        summary_rows = [
            {"metric": "Gross Revenue", "amount": pl["gross_revenue"]},
            {"metric": "Total Discounts", "amount": pl["total_discounts"]},
            {"metric": "Net Revenue", "amount": pl["net_revenue"]},
            {"metric": "Author Payouts", "amount": pl["author_payouts"]},
            {"metric": "Platform Gross Profit", "amount": pl["platform_gross_profit"]},
            {"metric": "Total Expenses", "amount": pl["total_expenses"]},
            {"metric": "Net Profit", "amount": pl["net_profit"]},
            {"metric": "Units Sold", "amount": pl["total_units_sold"]},
        ]
        sheets = [
            {
                "name": "Summary",
                "rows": summary_rows,
                "columns": [("Metric", "metric"), ("Amount (KES)", "amount", "#,##0.00")],
            },
            {
                "name": "Expenses by Category",
                "rows": expenses,
                "columns": [
                    ("Category", "category_name"),
                    ("Total (KES)", "total", "#,##0.00"),
                    ("Count", "count"),
                ],
            },
        ]
        return export_service.render_xlsx_multi(sheets, f"PL-{start}-{end}.xlsx")

    # ------------------------------------------------------------------
    # Inventory / Stock
    # ------------------------------------------------------------------

    def _grouped_stock(self):
        stock_by_location = {}
        for row in report_service.stock_by_location():
            stock_by_location.setdefault(row["location_name"], []).append(row)
        return stock_by_location

    def inventory_view(self, request):
        if not is_inventory_manager(request.user):
            raise PermissionDenied
        context = self._ctx(
            request, "Inventory / Stock", "inventory",
            stock_by_location=self._grouped_stock(),
            valuation=report_service.inventory_valuation_by_book(),
            as_of=date.today(),
        )
        return render(request, "inventory/admin/reports_inventory.html", context)

    def inventory_pdf_view(self, request):
        if not is_inventory_manager(request.user):
            raise PermissionDenied
        context = {
            "stock_by_location": self._grouped_stock(),
            "valuation": report_service.inventory_valuation_by_book(),
            "generated_at": timezone.now(),
        }
        return pdf_service.render_pdf(
            "inventory/pdf/inventory_report.html", context, f"inventory-report-{date.today()}.pdf",
        )

    def inventory_xlsx_view(self, request):
        if not is_inventory_manager(request.user):
            raise PermissionDenied
        sheets = [
            {
                "name": "Stock by Location",
                "rows": report_service.stock_by_location(),
                "columns": [
                    ("Location", "location_name"), ("Book", "book_title"), ("Author", "author_name"),
                    ("On Hand", "on_hand"), ("Reserved", "reserved"), ("Available", "available"),
                ],
            },
            {
                "name": "Valuation",
                "rows": report_service.inventory_valuation_by_book(),
                "columns": [
                    ("Book", "book_title"), ("Author", "author_name"), ("Location", "location"),
                    ("On Hand", "on_hand"), ("Unit Price (KES)", "unit_price", "#,##0.00"),
                    ("Value (KES)", "value", "#,##0.00"),
                ],
            },
        ]
        return export_service.render_xlsx_multi(sheets, f"inventory-report-{date.today()}.xlsx")

    # ------------------------------------------------------------------
    # Sales
    # ------------------------------------------------------------------

    def sales_view(self, request):
        if not is_inventory_or_finance_manager(request.user):
            raise PermissionDenied
        start, end = _parse_date_range(request)
        channel = request.GET.get("channel", "")
        # Only auto-pick when the user hasn't explicitly chosen one — a
        # hardcoded "month" default collapsed to a single point for the
        # common case (default view = month-to-date, so always ≤31 days).
        granularity = request.GET.get("granularity") or _default_granularity(start, end)
        context = self._ctx(
            request, "Sales", "sales",
            start=start, end=end, channel=channel, granularity=granularity,
            channels=Sale.CHANNELS,
            by_channel=report_service.sales_by_channel(start, end),
            by_book=report_service.sales_by_book(start, end, channel=channel or None),
            revenue_trend=report_service.revenue_over_time(start, end, granularity, channel=channel or None),
        )
        return render(request, "inventory/admin/reports_sales.html", context)

    def sales_xlsx_view(self, request):
        if not is_inventory_or_finance_manager(request.user):
            raise PermissionDenied
        start, end = _parse_date_range(request)
        channel = request.GET.get("channel", "") or None
        sheets = [
            {
                "name": "By Channel",
                "rows": report_service.sales_by_channel(start, end),
                "columns": [
                    ("Channel", "sale_channel"), ("Sales", "count"),
                    ("Net (KES)", "total_net", "#,##0.00"), ("Profit (KES)", "total_profit", "#,##0.00"),
                ],
            },
            {
                "name": "By Book",
                "rows": report_service.sales_by_book(start, end, channel=channel),
                "columns": [
                    ("Book", "book_title"), ("Units", "total_units"),
                    ("Net (KES)", "total_net", "#,##0.00"), ("Author Earnings (KES)", "author_earnings", "#,##0.00"),
                    ("Platform Profit (KES)", "platform_profit", "#,##0.00"),
                ],
            },
        ]
        return export_service.render_xlsx_multi(sheets, f"sales-{start}-{end}.xlsx")

    # ------------------------------------------------------------------
    # Books performance
    # ------------------------------------------------------------------

    def books_view(self, request):
        if not is_inventory_or_finance_manager(request.user):
            raise PermissionDenied
        start, end = _parse_date_range(request)
        context = self._ctx(
            request, "Books Performance", "books",
            start=start, end=end,
            rows=report_service.book_performance_report(start, end),
        )
        return render(request, "inventory/admin/reports_books.html", context)

    def books_xlsx_view(self, request):
        if not is_inventory_or_finance_manager(request.user):
            raise PermissionDenied
        start, end = _parse_date_range(request)
        rows = report_service.book_performance_report(start, end)
        columns = [
            ("Book", "book_title"), ("Author", "author_name"), ("Units Sold", "total_units"),
            ("Net Sales (KES)", "total_net", "#,##0.00"), ("Author Earnings (KES)", "author_earnings", "#,##0.00"),
            ("Platform Profit (KES)", "platform_profit", "#,##0.00"), ("Current Stock", "current_stock"),
            ("Reorder Level", "reorder_level"),
        ]
        return export_service.render_xlsx(rows, columns, f"books-performance-{start}-{end}.xlsx")

    # ------------------------------------------------------------------
    # Stock Received
    # ------------------------------------------------------------------

    def _stock_received_filters(self, request):
        def parse_date(key):
            try:
                return date.fromisoformat(request.GET.get(key, ""))
            except ValueError:
                return None

        status = request.GET.get("status") or None
        if status not in report_service.STOCK_STATUS_LABELS:
            status = None
        location = request.GET.get("location", "")
        return {
            "status": status,
            "created_start": parse_date("created_start"),
            "created_end": parse_date("created_end"),
            "location_id": int(location) if location.isdigit() else None,
        }

    def stock_received_view(self, request):
        if not is_inventory_manager(request.user):
            raise PermissionDenied
        filters = self._stock_received_filters(request)
        report = report_service.stock_received_report(**filters)

        base_params = {
            key: value for key, value in {
                "created_start": filters["created_start"] and filters["created_start"].isoformat(),
                "created_end": filters["created_end"] and filters["created_end"].isoformat(),
                "location": filters["location_id"],
            }.items() if value
        }
        counts = report["counts"]
        tiles = [
            {"key": "", "label": "All books", "count": counts["total"]},
            *(
                {"key": key, "label": label, "count": counts[key]}
                for key, label in report_service.STOCK_STATUS_LABELS.items()
            ),
        ]
        for tile in tiles:
            params = {**base_params, **({"status": tile["key"]} if tile["key"] else {})}
            tile["query"] = urlencode(params)
            tile["active"] = (filters["status"] or "") == tile["key"]

        context = self._ctx(
            request, "Stock Received", "stock_received",
            rows=report["rows"],
            tiles=tiles,
            filters=filters,
            status_label=report_service.STOCK_STATUS_LABELS.get(filters["status"]),
            locations=StockLocation.objects.filter(is_active=True).order_by("name"),
            export_query=request.GET.urlencode(),
        )
        return render(request, "inventory/admin/reports_stock_received.html", context)

    def _stock_received_columns(self):
        return [
            ("Book", "book_title"), ("Author", "author_name"), ("Created", "created", "yyyy-mm-dd"),
            ("Status", "status_label"), ("Total Received", "total_received"),
            ("Pending Approval Qty", "pending_qty"), ("First Received", "first_received", "yyyy-mm-dd"),
            ("Received At", "received_locations"), ("On Hand Now", "remaining"),
        ]

    def _stock_received_filename(self, filters, ext):
        suffix = f"-{filters['status'].replace('_', '-')}" if filters["status"] else ""
        return f"stock-received{suffix}-{date.today()}.{ext}"

    def stock_received_xlsx_view(self, request):
        if not is_inventory_manager(request.user):
            raise PermissionDenied
        filters = self._stock_received_filters(request)
        rows = report_service.stock_received_report(**filters)["rows"]
        return export_service.render_xlsx(
            rows, self._stock_received_columns(), self._stock_received_filename(filters, "xlsx"),
            sheet_name="Stock Received",
        )

    def stock_received_csv_view(self, request):
        if not is_inventory_manager(request.user):
            raise PermissionDenied
        filters = self._stock_received_filters(request)
        rows = report_service.stock_received_report(**filters)["rows"]
        return export_service.render_csv(
            rows, self._stock_received_columns(), self._stock_received_filename(filters, "csv"),
        )

    # ------------------------------------------------------------------
    # Author sales & amount owed
    # ------------------------------------------------------------------

    def _resolve_author_detail(self, request):
        """Bind the filter form from GET params; resolve payee/book/date range."""
        form = AuthorReportFilterForm(request.GET or None)
        if not request.GET or not form.is_valid():
            return form, None

        payee = form.cleaned_data.get("author") or form.cleaned_data.get("publisher")
        book = form.cleaned_data.get("book")
        if book and not payee:
            payee = book.publisher or book.author
        start = form.cleaned_data.get("start_date") or date.today().replace(month=1, day=1)
        end = form.cleaned_data.get("end_date") or date.today()

        detail = report_service.author_earnings_detail(payee, start, end, book=book)
        outstanding = payout_service.get_author_outstanding_balance(payee)
        return form, {"detail": detail, "outstanding": outstanding}

    def authors_view(self, request):
        if not is_finance_manager(request.user):
            raise PermissionDenied
        form, result = self._resolve_author_detail(request)
        context = self._ctx(
            request, "Author Sales & Amount Owed", "authors",
            form=form, result=result, media=self.media + form.media,
        )
        return render(request, "inventory/admin/reports_authors.html", context)

    def authors_pdf_view(self, request):
        if not is_finance_manager(request.user):
            raise PermissionDenied

        payout_id = request.GET.get("payout_id")
        if payout_id:
            payout = AuthorPayout.objects.filter(pk=payout_id).select_related("author", "publisher").first()
            if not payout:
                messages.error(request, "Payout not found.")
                return redirect("admin:inventory_reportshub_authors")
            context = {"payout": payout, "generated_at": timezone.now()}
            filename = f"statement-{payout.payee.name.replace(' ', '-')}-{payout.period_end}.pdf"
            return pdf_service.render_pdf("inventory/pdf/author_statement.html", context, filename)

        form, result = self._resolve_author_detail(request)
        if not result:
            messages.error(request, "Choose an author, a publisher, or a book title first.")
            return redirect("admin:inventory_reportshub_authors")
        detail = result["detail"]
        context = {"detail": detail, "generated_at": timezone.now()}
        filename = f"statement-{detail['payee'].name.replace(' ', '-')}-{detail['period_end']}.pdf"
        return pdf_service.render_pdf("inventory/pdf/author_statement.html", context, filename)

    def authors_xlsx_view(self, request):
        if not is_finance_manager(request.user):
            raise PermissionDenied
        form, result = self._resolve_author_detail(request)
        if not result:
            messages.error(request, "Choose an author, a publisher, or a book title first.")
            return redirect("admin:inventory_reportshub_authors")
        detail = result["detail"]
        rows = [
            {
                "date": item.sale.sale_date,
                "book": item.book.title,
                "sold_at": item.sale.sold_at_label,
                "quantity": item.quantity,
                "net_amount": item.agreed_net_amount,
                "earnings": item.author_payout_amount,
            }
            for item in detail["sale_items"]
        ]
        columns = [
            ("Date", "date"), ("Book", "book"), ("Sold At", "sold_at"), ("Qty", "quantity"),
            ("Net Sale (KES)", "net_amount", "#,##0.00"),
            (f"{detail['payee_label']} Earnings (KES)", "earnings", "#,##0.00"),
        ]
        filename = f"statement-{detail['payee'].name.replace(' ', '-')}-{detail['period_end']}.xlsx"
        return export_service.render_xlsx(rows, columns, filename, sheet_name="Earnings")

    def authors_send_view(self, request):
        if not is_finance_manager(request.user):
            raise PermissionDenied
        if request.method != "POST":
            return redirect("admin:inventory_reportshub_authors")

        author_id = request.POST.get("author_id")
        publisher_id = request.POST.get("publisher_id")
        book_id = request.POST.get("book_id") or None
        start_str = request.POST.get("start_date")
        end_str = request.POST.get("end_date")

        try:
            if publisher_id:
                payee = Publisher.objects.get(pk=publisher_id)
            else:
                payee = Author.objects.get(pk=author_id)
            book = Book.objects.get(pk=book_id) if book_id else None
            start = date.fromisoformat(start_str)
            end = date.fromisoformat(end_str)
        except (Author.DoesNotExist, Publisher.DoesNotExist, Book.DoesNotExist, TypeError, ValueError):
            messages.error(request, "Could not send statement — invalid author/publisher, book, or date range.")
            return redirect("admin:inventory_reportshub_authors")

        if not payee.email:
            messages.error(request, f"{payee.name} has no email address on file.")
        else:
            ok = send_author_statement_email(payee=payee, start=start, end=end, book=book)
            if ok:
                messages.success(request, f"Statement emailed to {payee.email}.")
            else:
                messages.error(request, "Failed to send email — details are in the System Log.")

        params = {"start_date": start.isoformat(), "end_date": end.isoformat()}
        if publisher_id:
            params["publisher"] = payee.pk
        else:
            params["author"] = payee.pk
        if book:
            params["book"] = book.pk
        url = f"{reverse('admin:inventory_reportshub_authors')}?{urlencode(params)}"
        return redirect(url)
