"""Invoice service — business logic for billing/invoices."""
from __future__ import annotations

import csv
import io
import logging
from datetime import date, datetime
from decimal import Decimal
from typing import AsyncGenerator, List, Optional, Tuple
from uuid import UUID

from src.core.config import settings

from sqlalchemy import func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload

from src.apps.billing.models.invoice import Invoice
from src.apps.billing.models.invoice_payment import InvoicePayment
from src.core.exceptions import ForbiddenError, NotFoundError

logger = logging.getLogger(__name__)


class InvoiceService:
    # ── Contract Invoices (INDL-28) ───────────────────────────────────────────

    @staticmethod
    async def create_contract_invoices(
        db: AsyncSession,
        tenant_id: UUID | str,
        contract_id: UUID,
        payment_plan_type: str,
        total_amount: Decimal,
        purchaser_name: Optional[str],
        purchaser_email: Optional[str],
    ) -> List[Invoice]:
        """Create 1 invoice (full) or 2 invoices (deposit_50) for a contract.

        Returns the created Invoice ORM objects (flushed but not committed).
        Caller is responsible for the enclosing transaction.
        """
        today = date.today()
        year = today.year

        base_count = (
            await db.execute(
                select(func.count()).select_from(Invoice).where(Invoice.tenant_id == tenant_id)
            )
        ).scalar_one()

        invoices: List[Invoice] = []

        if payment_plan_type == "deposit_50":
            half = (total_amount / 2).quantize(Decimal("0.01"))
            remainder = total_amount - half

            deposit = Invoice(
                tenant_id=tenant_id,
                contract_id=contract_id,
                invoice_number=f"INV-{year}-{base_count + 1:03d}",
                status="outstanding",
                total_amount=half,
                paid_amount=Decimal("0"),
                balance_due=half,
                due_date=today,
                purchaser_name=purchaser_name,
                purchaser_email=purchaser_email,
            )
            db.add(deposit)
            await db.flush()
            invoices.append(deposit)

            balance = Invoice(
                tenant_id=tenant_id,
                contract_id=contract_id,
                invoice_number=f"INV-{year}-{base_count + 2:03d}",
                status="outstanding",
                total_amount=remainder,
                paid_amount=Decimal("0"),
                balance_due=remainder,
                due_date=None,
                purchaser_name=purchaser_name,
                purchaser_email=purchaser_email,
            )
            db.add(balance)
            await db.flush()
            invoices.append(balance)
        else:
            # full payment
            invoice = Invoice(
                tenant_id=tenant_id,
                contract_id=contract_id,
                invoice_number=f"INV-{year}-{base_count + 1:03d}",
                status="outstanding",
                total_amount=total_amount,
                paid_amount=Decimal("0"),
                balance_due=total_amount,
                due_date=today,
                purchaser_name=purchaser_name,
                purchaser_email=purchaser_email,
            )
            db.add(invoice)
            await db.flush()
            invoices.append(invoice)

        return invoices

    # ── List ──────────────────────────────────────────────────────────────────

    async def get_list(
        self,
        db: AsyncSession,
        tenant_id: UUID | str,
        search: Optional[str] = None,
        status: Optional[str] = None,
        page: int = 1,
        page_size: int = 20,
    ) -> Tuple[List[Invoice], int]:
        """Paginated invoice list with optional search and status filter."""
        filters = [Invoice.tenant_id == tenant_id]

        if search:
            pattern = f"%{search}%"
            filters.append(
                or_(
                    Invoice.purchaser_name.ilike(pattern),
                    Invoice.invoice_number.ilike(pattern),
                )
            )

        if status:
            filters.append(Invoice.status == status)

        # Total count
        count_stmt = select(func.count()).select_from(Invoice).where(*filters)
        total: int = (await db.execute(count_stmt)).scalar_one()

        # Paginated rows — eager-load payments
        offset = (page - 1) * page_size
        stmt = (
            select(Invoice)
            .options(selectinload(Invoice.payments))
            .where(*filters)
            .order_by(Invoice.created_at.desc())
            .offset(offset)
            .limit(page_size)
        )
        result = await db.execute(stmt)
        items: List[Invoice] = list(result.scalars().all())

        return items, total

    # ── Single ────────────────────────────────────────────────────────────────

    async def get_by_id(
        self,
        db: AsyncSession,
        tenant_id: UUID | str,
        invoice_id: UUID,
    ) -> Invoice:
        """Fetch a single invoice with payments, raise NotFoundError if missing."""
        stmt = (
            select(Invoice)
            .options(selectinload(Invoice.payments))
            .where(
                Invoice.id == invoice_id,
                Invoice.tenant_id == tenant_id,
            )
        )
        invoice = (await db.execute(stmt)).scalar_one_or_none()
        if not invoice:
            raise NotFoundError("Invoice not found")
        return invoice

    # ── Record Payment ────────────────────────────────────────────────────────

    async def record_payment(
        self,
        db: AsyncSession,
        tenant_id: UUID | str,
        invoice_id: UUID,
        amount: Decimal,
        payment_date: date,
        payment_method: str,
        reference_number: Optional[str] = None,
        user_id: Optional[UUID] = None,
    ) -> Invoice:
        """Record a payment against an invoice and recalculate balances."""
        invoice = await self.get_by_id(db, tenant_id, invoice_id)

        if invoice.balance_due <= 0:
            raise ForbiddenError("Invoice already fully paid")

        payment = InvoicePayment(
            invoice_id=invoice.id,
            tenant_id=tenant_id,
            amount=amount,
            method=payment_method,
            received_on=payment_date,
            receipt_number=reference_number,
            recorded_by=user_id,
        )
        db.add(payment)

        new_paid = (invoice.paid_amount or Decimal("0")) + amount
        new_balance = invoice.total_amount - new_paid

        invoice.paid_amount = new_paid
        invoice.balance_due = max(Decimal("0"), new_balance)
        if new_balance <= 0:
            invoice.status = "paid"
        elif invoice.status != "overdue":
            invoice.status = "partial"
        # if already overdue and still has balance, leave as overdue

        await db.flush()
        return invoice

    # ── Send Reminder ─────────────────────────────────────────────────────────

    async def send_reminder(
        self,
        db: AsyncSession,
        tenant_id: UUID | str,
        invoice_id: UUID,
        user_id: Optional[UUID] = None,
    ) -> Invoice:
        """Log a reminder and update tracking fields. Email via SES is TODO."""
        invoice = await self.get_by_id(db, tenant_id, invoice_id)

        logger.info(
            "[billing] Reminder triggered for invoice %s (tenant=%s, count=%d)",
            invoice_id,
            tenant_id,
            invoice.reminder_count,
        )

        invoice.last_reminder_at = datetime.utcnow()
        invoice.reminder_count = (invoice.reminder_count or 0) + 1

        await db.flush()
        return invoice

    # ── CSV Injection Guard ───────────────────────────────────────────────────

    @staticmethod
    def _csv_safe(value: str | None) -> str:
        """Prefix formula-triggering characters to prevent CSV injection."""
        if value and value[0] in ('=', '+', '-', '@', '\t', '\r'):
            return '\t' + value
        return value or ''

    # ── Export CSV ────────────────────────────────────────────────────────────

    async def export_csv(
        self,
        db: AsyncSession,
        tenant_id: UUID | str,
        search: Optional[str] = None,
        status: Optional[str] = None,
    ) -> AsyncGenerator[str, None]:
        """Async generator yielding CSV rows (header first)."""
        # Fetch all matching invoices (no pagination for export)
        filters = [Invoice.tenant_id == tenant_id]
        if search:
            pattern = f"%{search}%"
            filters.append(
                or_(
                    Invoice.purchaser_name.ilike(pattern),
                    Invoice.invoice_number.ilike(pattern),
                )
            )
        if status:
            filters.append(Invoice.status == status)

        stmt = (
            select(Invoice)
            .where(*filters)
            .order_by(Invoice.created_at.desc())
        )
        result = await db.execute(stmt)
        invoices: List[Invoice] = list(result.scalars().all())

        # Yield rows via in-memory StringIO so csv.writer handles escaping
        buf = io.StringIO()
        writer = csv.writer(buf)

        writer.writerow(
            ["Invoice #", "Purchaser", "Total", "Paid", "Outstanding", "Due", "Status"]
        )
        yield buf.getvalue()
        buf.truncate(0)
        buf.seek(0)

        for inv in invoices:
            writer.writerow(
                [
                    self._csv_safe(inv.invoice_number),
                    self._csv_safe(inv.purchaser_name),
                    str(inv.total_amount),
                    str(inv.paid_amount),
                    str(inv.balance_due),
                    inv.due_date.isoformat() if inv.due_date else "",
                    inv.status,
                ]
            )
            yield buf.getvalue()
            buf.truncate(0)
            buf.seek(0)

    # ── Stripe: Create PaymentIntent ─────────────────────────────────────────

    async def create_payment_intent(
        self,
        db: AsyncSession,
        tenant_id: UUID | str,
        invoice_id: UUID,
    ) -> str:
        """Create a Stripe PaymentIntent for a failed invoice. Returns client_secret."""
        from src.apps.tenants.models.account import Account
        from src.core.exceptions import ForbiddenError, ValidationError

        invoice = await self.get_by_id(db, tenant_id, invoice_id)

        if invoice.status != "failed":
            raise ForbiddenError("Only invoices with status 'failed' can be paid via Stripe")
        if not invoice.balance_due or invoice.balance_due <= 0:
            raise ForbiddenError("Invoice has no balance due")

        account = (
            await db.execute(select(Account).where(Account.id == tenant_id))
        ).scalar_one_or_none()
        if not account:
            raise ForbiddenError("Account not found")

        if not settings.STRIPE_SECRET_KEY:
            raise ValidationError("Stripe is not configured")

        import stripe
        stripe.api_key = settings.STRIPE_SECRET_KEY

        amount_cents = int(invoice.balance_due * 100)
        intent = stripe.PaymentIntent.create(
            amount=amount_cents,
            currency="cad",
            customer=account.stripe_customer_id or None,
            metadata={
                "invoice_id": str(invoice.id),
                "tenant_id": str(tenant_id),
            },
        )

        invoice.stripe_payment_intent_id = intent.id
        await db.flush()
        return intent.client_secret

    # ── Stripe: Confirm Payment ───────────────────────────────────────────────

    async def confirm_payment(
        self,
        db: AsyncSession,
        tenant_id: UUID | str,
        invoice_id: UUID,
        payment_intent_id: str,
    ) -> Invoice:
        """Verify PaymentIntent succeeded with Stripe, then mark invoice paid."""
        from src.core.exceptions import ValidationError

        invoice = await self.get_by_id(db, tenant_id, invoice_id)

        if not settings.STRIPE_SECRET_KEY:
            raise ValidationError("Stripe is not configured")

        import stripe
        stripe.api_key = settings.STRIPE_SECRET_KEY

        intent = stripe.PaymentIntent.retrieve(payment_intent_id)
        if intent.status != "succeeded":
            raise ValidationError(f"PaymentIntent status is '{intent.status}', not 'succeeded'")

        invoice.status = "paid"
        invoice.paid_amount = invoice.total_amount
        invoice.balance_due = Decimal("0")
        await db.flush()
        return invoice

    # ── Invoice PDF ───────────────────────────────────────────────────────────

    async def generate_pdf(
        self,
        db: AsyncSession,
        tenant_id: UUID | str,
        invoice_id: UUID,
    ) -> bytes:
        """Generate a simple PDF for the invoice using fpdf2."""
        from fpdf import FPDF

        def _safe(v) -> str:
            if v is None:
                return ""
            s = str(v)
            return s.encode("latin-1", errors="replace").decode("latin-1")

        invoice = await self.get_by_id(db, tenant_id, invoice_id)

        pdf = FPDF()
        pdf.add_page()
        pdf.set_font("Helvetica", "B", 20)
        pdf.cell(0, 12, "INDELIS - Invoice", ln=True)

        pdf.set_font("Helvetica", "", 12)
        pdf.ln(4)
        pdf.cell(60, 8, "Invoice Number:", border=0)
        pdf.cell(0, 8, _safe(invoice.invoice_number), ln=True)

        issued = (invoice.due_date or invoice.created_at.date()).isoformat()
        pdf.cell(60, 8, "Date:")
        pdf.cell(0, 8, _safe(issued), ln=True)

        pdf.cell(60, 8, "Status:")
        pdf.cell(0, 8, _safe(invoice.status).upper(), ln=True)

        if invoice.purchaser_name:
            pdf.cell(60, 8, "Billed To:")
            pdf.cell(0, 8, _safe(invoice.purchaser_name), ln=True)

        pdf.ln(6)
        pdf.set_font("Helvetica", "B", 12)
        pdf.cell(0, 8, "Summary", ln=True)
        pdf.line(pdf.get_x(), pdf.get_y(), pdf.get_x() + 190, pdf.get_y())
        pdf.ln(1)

        pdf.set_font("Helvetica", "", 11)
        pdf.cell(100, 8, "INDELIS Subscription")
        pdf.cell(0, 8, f"CAD ${invoice.total_amount:.2f}", ln=True)

        pdf.set_font("Helvetica", "B", 11)
        pdf.cell(100, 8, "Total")
        pdf.cell(0, 8, f"CAD ${invoice.total_amount:.2f}", ln=True)
        pdf.cell(100, 8, "Paid")
        pdf.cell(0, 8, f"CAD ${invoice.paid_amount:.2f}", ln=True)
        pdf.cell(100, 8, "Balance Due")
        pdf.cell(0, 8, f"CAD ${invoice.balance_due:.2f}", ln=True)

        pdf.ln(8)
        pdf.set_font("Helvetica", "I", 9)
        pdf.multi_cell(0, 6, "All amounts in Canadian Dollars (CAD). HST included where applicable.")

        return pdf.output()

    # ── Billing CSV Export ────────────────────────────────────────────────────

    @staticmethod
    def _plan_name_from_amount(amount: float) -> str:
        if amount <= 149:
            return "Starter"
        if amount <= 349:
            return "Professional"
        return "Enterprise"

    async def export_billing_csv(
        self,
        db: AsyncSession,
        tenant_id: UUID | str,
        plan_name: str | None = None,
    ) -> AsyncGenerator[str, None]:
        """Billing-page CSV: Invoice, Date, Plan, Amount, Status."""
        filters = [Invoice.tenant_id == tenant_id]
        stmt = select(Invoice).where(*filters).order_by(Invoice.created_at.desc())
        result = await db.execute(stmt)
        invoices: List[Invoice] = list(result.scalars().all())

        buf = io.StringIO()
        writer = csv.writer(buf)
        writer.writerow(["Invoice", "Date", "Plan", "Amount", "Status"])
        yield buf.getvalue()
        buf.truncate(0)
        buf.seek(0)

        for inv in invoices:
            inv_date = (inv.due_date or inv.created_at.date()).isoformat()
            row_plan = plan_name or self._plan_name_from_amount(float(inv.total_amount))
            writer.writerow([
                self._csv_safe(inv.invoice_number),
                inv_date,
                row_plan,
                str(inv.total_amount),
                inv.status,
            ])
            yield buf.getvalue()
            buf.truncate(0)
            buf.seek(0)

    # ── Transition Overdue ────────────────────────────────────────────────────

    async def transition_overdue(
        self,
        db: AsyncSession,
        tenant_id: Optional[UUID | str] = None,
        reminders_out: Optional[list] = None,
    ) -> int:
        """
        Mark past-due invoices as 'overdue' and record which reminder emails are
        due at the 7 / 14 / 30-day thresholds (based on reminder_count).

        Idempotency (SEC API4): reminder_count is incremented here, in the same
        transaction the caller commits. Reminders are NOT enqueued here — each
        due reminder is appended to `reminders_out` (if provided) so the caller
        can COMMIT the counter bump first and enqueue only afterwards. That
        commit-then-enqueue ordering guarantees a crash can at worst MISS a
        reminder, never SEND A DUPLICATE to a grieving family (INDL-54e).

        Returns the number of invoices processed.
        """
        today = date.today()

        filters = [
            Invoice.status.in_(["outstanding", "partial"]),
            Invoice.due_date < today,
            Invoice.due_date.isnot(None),
        ]
        if tenant_id is not None:
            filters.append(Invoice.tenant_id == tenant_id)

        stmt = select(Invoice).where(*filters)
        result = await db.execute(stmt)
        invoices: List[Invoice] = list(result.scalars().all())

        processed = 0
        for invoice in invoices:
            # Transition to overdue
            invoice.status = "overdue"

            # Determine whether a 7 / 14 / 30-day reminder is due
            days_overdue = (today - invoice.due_date).days
            reminder_count = invoice.reminder_count or 0

            # reminder_count tracks how many reminders have been sent:
            #   0 → no reminders yet  → send at 7 days
            #   1 → 1 sent            → send at 14 days
            #   2 → 2 sent            → send at 30 days
            should_remind = (
                (reminder_count == 0 and days_overdue >= 7)
                or (reminder_count == 1 and days_overdue >= 14)
                or (reminder_count == 2 and days_overdue >= 30)
            )

            if should_remind:
                logger.info(
                    "[scheduler] Queuing reminder for invoice %s (day %d, count %d)",
                    invoice.id,
                    days_overdue,
                    reminder_count,
                )
                # Bump the idempotency counter in this (soon-to-be-committed)
                # transaction; the caller enqueues only after committing.
                invoice.last_reminder_at = datetime.utcnow()
                invoice.reminder_count = reminder_count + 1
                if reminders_out is not None:
                    reminders_out.append(
                        {
                            "invoice_id": str(invoice.id),
                            "tenant_id": str(invoice.tenant_id),
                            "days_overdue": days_overdue,
                        }
                    )

            processed += 1

        if invoices:
            await db.flush()

        return processed
