import json
from decimal import Decimal
from datetime import datetime, date
from typing import Optional

from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session, joinedload

from app.core.database import get_db
from app.core.models import Invoice, InvoiceItem, InvoiceEvent, InvoiceStatus, Payment
from app.services.invoice_service import (
    generate_invoice_number,
    calculate_item,
    recalculate_invoice,
    issue_invoice as _issue_invoice,
    void_invoice as _void_invoice,
    record_payment as _record_payment,
    write_invoice_event,
)
from app.services.invoice_pdf import generate_invoice_pdf
from fastapi.responses import Response

router = APIRouter(prefix="/api/invoices", tags=["invoices"])


# ===== Request Models =====

class InvoiceItemCreate(BaseModel):
    product_id: int | None = None
    description: str | None = None
    quantity: float = 1.0
    unit_price: float = 0.0
    tax_rate: float = 0.15


class InvoiceCreate(BaseModel):
    customer_id: int
    invoice_date: date
    due_date: date | None = None
    currency: str = "NZD"
    notes: str | None = None
    items: list[InvoiceItemCreate] = []


class InvoiceItemUpdate(BaseModel):
    product_id: int | None = None
    description: str | None = None
    quantity: float | None = None
    unit_price: float | None = None
    tax_rate: float | None = None


class InvoiceUpdate(BaseModel):
    due_date: date | None = None
    notes: str | None = None
    items: list[InvoiceItemCreate] = []


class PaymentCreate(BaseModel):
    amount: float
    method: str = "bank_transfer"
    reference: str = ""
    notes: str = ""


# ===== Response Models =====

class InvoiceItemResponse(BaseModel):
    id: int
    invoice_id: int
    product_id: int | None
    description: str | None
    quantity: float
    unit_price: float
    tax_rate: float
    subtotal: float
    tax_amount: float
    total: float

    class Config:
        from_attributes = True


class CustomerSimple(BaseModel):
    id: int
    name: str
    email: str | None

    class Config:
        from_attributes = True


class InvoiceResponse(BaseModel):
    id: int
    invoice_number: str
    customer_id: int
    customer: CustomerSimple | None
    invoice_date: date
    due_date: date | None
    subtotal: float
    total_tax: float
    total_amount: float
    currency: str
    status: str
    notes: str | None
    issued_at: datetime | None
    sent_at: datetime | None
    paid_at: datetime | None
    voided_at: datetime | None
    created_at: datetime | None
    items: list[InvoiceItemResponse] = []

    class Config:
        from_attributes = True


class InvoiceEventResponse(BaseModel):
    id: int
    invoice_id: int
    event_type: str
    actor_type: str
    actor_name: str | None
    message: str | None
    metadata_json: str | None
    created_at: datetime | None

    class Config:
        from_attributes = True


class PaymentResponse(BaseModel):
    id: int
    invoice_id: int
    paid_at: datetime | None
    amount: float
    currency: str
    method: str
    reference: str | None
    notes: str | None
    created_at: datetime | None

    class Config:
        from_attributes = True


# ===== Helper =====

def _invoice_to_response(invoice: Invoice) -> InvoiceResponse:
    items = [
        InvoiceItemResponse(
            id=i.id,
            invoice_id=i.invoice_id,
            product_id=i.product_id,
            description=i.description,
            quantity=float(i.quantity),
            unit_price=float(i.unit_price),
            tax_rate=float(i.tax_rate),
            subtotal=float(i.subtotal),
            tax_amount=float(i.tax_amount),
            total=float(i.total),
        )
        for i in invoice.items
    ]
    return InvoiceResponse(
        id=invoice.id,
        invoice_number=invoice.invoice_number,
        customer_id=invoice.customer_id,
        customer=CustomerSimple.model_validate(invoice.customer) if invoice.customer else None,
        invoice_date=invoice.invoice_date,
        due_date=invoice.due_date,
        subtotal=float(invoice.subtotal),
        total_tax=float(invoice.total_tax),
        total_amount=float(invoice.total_amount),
        currency=invoice.currency,
        status=invoice.status.value,
        notes=invoice.notes,
        issued_at=invoice.issued_at,
        sent_at=invoice.sent_at,
        paid_at=invoice.paid_at,
        voided_at=invoice.voided_at,
        created_at=invoice.created_at,
        items=items,
    )


# ===== Endpoints =====

@router.get("", response_model=list[InvoiceResponse])
def list_invoices(
    page: int = Query(1, ge=1),
    page_size: int = Query(20, ge=1, le=100),
    customer_id: int | None = None,
    status: str | None = None,
    date_from: date | None = None,
    date_to: date | None = None,
    db: Session = Depends(get_db),
):
    q = db.query(Invoice).options(joinedload(Invoice.customer))
    if customer_id:
        q = q.filter(Invoice.customer_id == customer_id)
    if status:
        q = q.filter(Invoice.status == status)
    if date_from:
        q = q.filter(Invoice.invoice_date >= date_from)
    if date_to:
        q = q.filter(Invoice.invoice_date <= date_to)

    q = q.order_by(Invoice.created_at.desc())
    offset = (page - 1) * page_size
    invoices = q.offset(offset).limit(page_size).all()
    return [_invoice_to_response(i) for i in invoices]


@router.post("", response_model=InvoiceResponse, status_code=201)
def create_invoice(data: InvoiceCreate, db: Session = Depends(get_db)):
    invoice_number = generate_invoice_number(db)

    invoice = Invoice(
        invoice_number=invoice_number,
        customer_id=data.customer_id,
        invoice_date=data.invoice_date,
        due_date=data.due_date,
        currency=data.currency,
        notes=data.notes,
        status=InvoiceStatus.draft,
    )
    db.add(invoice)
    db.flush()  # get invoice.id

    for item_data in data.items:
        item_calc = calculate_item(item_data.model_dump())
        item = InvoiceItem(
            invoice_id=invoice.id,
            product_id=item_data.product_id,
            description=item_data.description,
            quantity=item_data.quantity,
            unit_price=item_data.unit_price,
            tax_rate=item_data.tax_rate,
            subtotal=item_calc['subtotal'],
            tax_amount=item_calc['tax_amount'],
            total=item_calc['total'],
        )
        db.add(item)

    db.flush()
    recalculate_invoice(db, invoice)
    write_invoice_event(db, invoice.id, "created", "user", "system", "Invoice created")
    db.refresh(invoice)

    return _invoice_to_response(invoice)


@router.get("/{invoice_id}", response_model=InvoiceResponse)
def get_invoice(invoice_id: int, db: Session = Depends(get_db)):
    invoice = (
        db.query(Invoice)
        .options(joinedload(Invoice.customer), joinedload(Invoice.items))
        .filter(Invoice.id == invoice_id)
        .first()
    )
    if not invoice:
        raise HTTPException(status_code=404, detail="Invoice not found")
    return _invoice_to_response(invoice)


@router.put("/{invoice_id}", response_model=InvoiceResponse)
def update_invoice(invoice_id: int, data: InvoiceUpdate, db: Session = Depends(get_db)):
    invoice = db.query(Invoice).filter(Invoice.id == invoice_id).first()
    if not invoice:
        raise HTTPException(status_code=404, detail="Invoice not found")

    if invoice.status != InvoiceStatus.draft:
        raise HTTPException(status_code=400, detail=f"Cannot edit invoice in status: {invoice.status}")

    if data.due_date is not None:
        invoice.due_date = data.due_date
    if data.notes is not None:
        invoice.notes = data.notes

    if data.items is not None:
        # Replace items
        db.query(InvoiceItem).filter(InvoiceItem.invoice_id == invoice_id).delete()
        for item_data in data.items:
            item_calc = calculate_item(item_data.model_dump())
            item = InvoiceItem(
                invoice_id=invoice.id,
                product_id=item_data.product_id,
                description=item_data.description,
                quantity=item_data.quantity,
                unit_price=item_data.unit_price,
                tax_rate=item_data.tax_rate,
                subtotal=item_calc['subtotal'],
                tax_amount=item_calc['tax_amount'],
                total=item_calc['total'],
            )
            db.add(item)
        recalculate_invoice(db, invoice)

    db.commit()
    db.refresh(invoice)
    return _invoice_to_response(invoice)


@router.delete("/{invoice_id}", status_code=204)
def delete_invoice(invoice_id: int, db: Session = Depends(get_db)):
    invoice = db.query(Invoice).filter(Invoice.id == invoice_id).first()
    if not invoice:
        raise HTTPException(status_code=404, detail="Invoice not found")
    if invoice.status != InvoiceStatus.draft:
        raise HTTPException(status_code=400, detail=f"Cannot delete invoice in status: {invoice.status}")
    db.delete(invoice)
    db.commit()


@router.post("/{invoice_id}/issue", response_model=InvoiceResponse)
def issue_invoice(invoice_id: int, db: Session = Depends(get_db)):
    try:
        invoice = _issue_invoice(db, invoice_id)
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))
    db.refresh(invoice)
    return _invoice_to_response(invoice)


@router.post("/{invoice_id}/send", response_model=InvoiceResponse)
def send_invoice(invoice_id: int, db: Session = Depends(get_db)):
    invoice = db.query(Invoice).filter(Invoice.id == invoice_id).first()
    if not invoice:
        raise HTTPException(status_code=404, detail="Invoice not found")
    if invoice.status not in (InvoiceStatus.issued,):
        raise HTTPException(status_code=400, detail=f"Cannot send invoice in status: {invoice.status}")

    invoice.status = InvoiceStatus.sent
    invoice.sent_at = datetime.now()
    db.commit()
    write_invoice_event(db, invoice_id, "sent", "user", "system", "Invoice marked as sent")
    db.refresh(invoice)
    return _invoice_to_response(invoice)


@router.post("/{invoice_id}/void", response_model=InvoiceResponse)
def void_invoice(invoice_id: int, db: Session = Depends(get_db)):
    try:
        invoice = _void_invoice(db, invoice_id)
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))
    db.refresh(invoice)
    return _invoice_to_response(invoice)


@router.post("/{invoice_id}/payments", response_model=PaymentResponse, status_code=201)
def add_payment(invoice_id: int, data: PaymentCreate, db: Session = Depends(get_db)):
    try:
        payment = _record_payment(
            db,
            invoice_id,
            Decimal(str(data.amount)),
            method=data.method,
            reference=data.reference,
            notes=data.notes,
        )
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))

    return PaymentResponse(
        id=payment.id,
        invoice_id=payment.invoice_id,
        paid_at=payment.paid_at,
        amount=float(payment.amount),
        currency=payment.currency,
        method=payment.method,
        reference=payment.reference,
        notes=payment.notes,
        created_at=payment.created_at,
    )


@router.get("/{invoice_id}/events", response_model=list[InvoiceEventResponse])
def get_invoice_events(invoice_id: int, db: Session = Depends(get_db)):
    events = (
        db.query(InvoiceEvent)
        .filter(InvoiceEvent.invoice_id == invoice_id)
        .order_by(InvoiceEvent.created_at.asc())
        .all()
    )
    return [
        InvoiceEventResponse(
            id=e.id,
            invoice_id=e.invoice_id,
            event_type=e.event_type,
            actor_type=e.actor_type,
            actor_name=e.actor_name,
            message=e.message,
            metadata_json=e.metadata_json,
            created_at=e.created_at,
        )
        for e in events
    ]


from pydantic import BaseModel


@router.get("/{invoice_id}/pdf")
def get_invoice_pdf(
    invoice_id: int,
    db: Session = Depends(get_db),
    current_user = Depends(get_current_user),
):
    """Download invoice as PDF."""
    pdf_bytes = generate_invoice_pdf(db, invoice_id)
    return Response(
        content=pdf_bytes,
        media_type="application/pdf",
        headers={"Content-Disposition": f"inline; filename=invoice-{invoice_id}.pdf"}
    )
