from datetime import date

from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import Response
from sqlalchemy.exc import IntegrityError
from sqlalchemy import func
from sqlalchemy.orm import Session, selectinload

from app.database import get_db
from app.deps import require_permission
from app.models import BookingChannel, Client, CompanySetting, Enquiry, EnquiryFollowup, EnquiryProduct, Event, Expense, Invoice, LeadSource, Payment, Product, Quotation, QuotationItem, QuotationSetting, User
from app.schemas import (
    BookingChannelIn,
    BookingChannelOut,
    ClientIn,
    ClientOut,
    EnquiryIn,
    EnquiryOut,
    FollowupIn,
    FollowupOut,
    LeadSourceIn,
    LeadSourceOut,
    LineItemOut,
    QuotationIn,
    QuotationOut,
    QuotationFromEnquiryIn,
)
from app.quotation_pdf import build_quotation_pdf, quotation_reference
from app.utils import line_amount, money, next_public_id

router = APIRouter(tags=["crm"])

TRANSPORT_PREFIX = "__transport__:"


def _encode_terms(transport, terms: str | None) -> str | None:
    if transport <= 0 and not terms:
        return None
    note = terms or ""
    if transport > 0:
        return f"{TRANSPORT_PREFIX}{transport}\n{note}".strip()
    return note or None


def _decode_transport(terms: str | None) -> tuple:
    if not terms or not terms.startswith(TRANSPORT_PREFIX):
        return money(0), terms
    first, _, rest = terms.partition("\n")
    raw = first[len(TRANSPORT_PREFIX) :]
    note = rest.strip() or None
    return money(raw), note


def _monthly_quotation_sequence(db: Session, when: date) -> int:
    start = date(when.year, when.month, 1)
    if when.month == 12:
        end = date(when.year + 1, 1, 1)
    else:
        end = date(when.year, when.month + 1, 1)
    count = db.query(func.count(Quotation.id)).filter(Quotation.quotation_date >= start, Quotation.quotation_date < end).scalar()
    return int(count or 0) + 1


def serialize_enquiry(row: Enquiry, event: Event | None = None) -> EnquiryOut:
    data = EnquiryOut.model_validate(row)
    data.products = [
        LineItemOut(
            id=item.id,
            product_id=item.product_id,
            name=item.name,
            quantity=item.quantity,
            unit_price=item.unit_price,
            discount=item.discount,
            amount=item.amount,
        )
        for item in (row.products or [])
    ]
    data.event_id = event.id if event else None
    data.event_public_id = event.public_id if event else None
    return data


def _apply_enquiry_products(db: Session, enquiry: Enquiry, payload: EnquiryIn) -> None:
    rows: list[EnquiryProduct] = []
    for item in payload.products:
        if item.quantity <= 0:
            continue

        product = None
        product_id = item.product_id
        if product_id:
            product = db.query(Product).filter(Product.id == product_id).first()
            if product is None:
                # Avoid FK crashes when stale/deleted product ids arrive from clients.
                product_id = None

        name = (item.name or "").strip() or (product.name if product else "")
        if not name:
            continue

        unit_price = item.unit_price if item.unit_price is not None else (product.base_price if product else 0)
        discount = item.discount if item.discount is not None else 0

        rows.append(
            EnquiryProduct(
                product_id=product_id,
                name=name,
                quantity=item.quantity,
                unit_price=unit_price,
                discount=discount,
                amount=line_amount(item.quantity, unit_price, discount),
            )
        )

    enquiry.products = rows


def _client_stats(db: Session, client_id: int) -> dict:
    enquiries = db.query(Enquiry).filter(Enquiry.client_id == client_id).all()
    events = db.query(Event).filter(Event.client_id == client_id).all()
    invoices = db.query(Invoice).filter(Invoice.client_id == client_id, Invoice.status != "Cancelled").all()
    payments = db.query(Payment).filter(Payment.client_id == client_id).all()
    expenses = db.query(Expense).filter(Expense.event_id.in_([e.id for e in events] or [0])).all()
    revenue = sum((money(inv.final_amount) for inv in invoices), money(0))
    paid = sum((money(p.amount) for p in payments), money(0))
    cost = sum((money(x.amount) for x in expenses), money(0))
    return {
        "total_enquiries": len(enquiries),
        "total_events": len(events),
        "completed_events": len([e for e in events if e.status == "Completed"]),
        "cancelled_events": len([e for e in events if e.status == "Cancelled"]),
        "total_revenue": revenue,
        "total_paid": paid,
        "outstanding_amount": money(revenue - paid),
        "total_expenses": cost,
        "total_profit": money(revenue - cost),
    }


@router.get("/lead-sources", response_model=list[LeadSourceOut])
def list_lead_sources(db: Session = Depends(get_db), _: User = Depends(require_permission("enquiry.view"))):
    return db.query(LeadSource).all()


@router.get("/lead-sources/{lead_source_id}", response_model=LeadSourceOut)
def get_lead_source(lead_source_id: int, db: Session = Depends(get_db), _: User = Depends(require_permission("enquiry.view"))):
    row = db.query(LeadSource).filter(LeadSource.id == lead_source_id).first()
    if row is None:
        raise HTTPException(status_code=404, detail="Lead source not found")
    return row


@router.post("/lead-sources", response_model=LeadSourceOut)
def create_lead_source(payload: LeadSourceIn, db: Session = Depends(get_db), _: User = Depends(require_permission("enquiry.create"))):
    row = LeadSource(**payload.model_dump())
    db.add(row)
    db.commit()
    db.refresh(row)
    return row


@router.get("/booking-channels", response_model=list[BookingChannelOut])
def list_booking_channels(db: Session = Depends(get_db), _: User = Depends(require_permission("enquiry.view"))):
    return db.query(BookingChannel).all()


@router.get("/booking-channels/{booking_channel_id}", response_model=BookingChannelOut)
def get_booking_channel(
    booking_channel_id: int,
    db: Session = Depends(get_db),
    _: User = Depends(require_permission("enquiry.view")),
):
    row = db.query(BookingChannel).filter(BookingChannel.id == booking_channel_id).first()
    if row is None:
        raise HTTPException(status_code=404, detail="Booking channel not found")
    return row


@router.post("/booking-channels", response_model=BookingChannelOut)
def create_booking_channel(payload: BookingChannelIn, db: Session = Depends(get_db), _: User = Depends(require_permission("enquiry.create"))):
    row = BookingChannel(**payload.model_dump())
    db.add(row)
    db.commit()
    db.refresh(row)
    return row


@router.get("/clients", response_model=list[ClientOut])
def list_clients(db: Session = Depends(get_db), _: User = Depends(require_permission("client.view"))):
    rows = db.query(Client).order_by(Client.id.desc()).all()
    out = []
    for row in rows:
        item = ClientOut.model_validate(row)
        item.stats = _client_stats(db, row.id)
        out.append(item)
    return out


@router.get("/clients/{client_id}", response_model=ClientOut)
def get_client(client_id: int, db: Session = Depends(get_db), _: User = Depends(require_permission("client.view"))):
    row = db.query(Client).filter(Client.id == client_id).first()
    if row is None:
        raise HTTPException(status_code=404, detail="Client not found")
    item = ClientOut.model_validate(row)
    item.stats = _client_stats(db, row.id)
    return item


@router.post("/clients", response_model=ClientOut)
def create_client(payload: ClientIn, db: Session = Depends(get_db), _: User = Depends(require_permission("client.create"))):
    row = Client(public_id=next_public_id(db, Client, "CLT"), **payload.model_dump())
    db.add(row)
    db.commit()
    db.refresh(row)
    return row


@router.patch("/clients/{client_id}", response_model=ClientOut)
def update_client(client_id: int, payload: ClientIn, db: Session = Depends(get_db), _: User = Depends(require_permission("client.edit"))):
    row = db.query(Client).filter(Client.id == client_id).first()
    if row is None:
        raise HTTPException(status_code=404, detail="Client not found")
    for key, value in payload.model_dump().items():
        setattr(row, key, value)
    db.commit()
    db.refresh(row)
    return row


@router.delete("/clients/{client_id}")
def delete_client(client_id: int, db: Session = Depends(get_db), _: User = Depends(require_permission("client.delete"))):
    row = db.query(Client).filter(Client.id == client_id).first()
    if row is None:
        raise HTTPException(status_code=404, detail="Client not found")
    db.delete(row)
    db.commit()
    return {"ok": True}


def _sync_client_from_enquiry(db: Session, payload: EnquiryIn) -> Client:
    client = None
    if payload.client_id:
        client = db.query(Client).filter(Client.id == payload.client_id).first()
    if client is None:
        client = db.query(Client).filter(Client.phone == payload.phone).first()
    if client is None:
        client = Client(
            public_id=next_public_id(db, Client, "CLT"),
            name=payload.client_name,
            company_name=payload.company_name,
            client_type="Individual",
            phone=payload.phone,
            email=payload.email,
            address=payload.location,
            district=payload.district,
            city=payload.city,
            lead_source=payload.lead_source,
            booking_channel=payload.booking_channel,
            first_enquiry_date=date.today(),
            last_enquiry_date=date.today(),
            status="Active",
            notes=None,
        )
        db.add(client)
        db.flush()
    else:
        client.last_enquiry_date = date.today()
        if client.first_enquiry_date is None:
            client.first_enquiry_date = date.today()
    return client


@router.get("/enquiries", response_model=list[EnquiryOut])
def list_enquiries(status: str | None = None, db: Session = Depends(get_db), _: User = Depends(require_permission("enquiry.view"))):
    query = db.query(Enquiry).options(selectinload(Enquiry.products))
    if status:
        query = query.filter(Enquiry.status == status)
    rows = query.order_by(Enquiry.id.desc()).all()
    enquiry_ids = [row.id for row in rows]
    events = (
        db.query(Event)
        .filter(Event.enquiry_id.in_(enquiry_ids or [0]))
        .order_by(Event.id.desc())
        .all()
    )
    event_by_enquiry = {}
    for event in events:
        if event.enquiry_id not in event_by_enquiry:
            event_by_enquiry[event.enquiry_id] = event
    return [serialize_enquiry(row, event_by_enquiry.get(row.id)) for row in rows]


@router.get("/enquiries/{enquiry_id}", response_model=EnquiryOut)
def get_enquiry(enquiry_id: int, db: Session = Depends(get_db), _: User = Depends(require_permission("enquiry.view"))):
    row = db.query(Enquiry).options(selectinload(Enquiry.products)).filter(Enquiry.id == enquiry_id).first()
    if row is None:
        raise HTTPException(status_code=404, detail="Enquiry not found")
    event = db.query(Event).filter(Event.enquiry_id == row.id).order_by(Event.id.desc()).first()
    return serialize_enquiry(row, event)


@router.post("/enquiries", response_model=EnquiryOut)
def create_enquiry(payload: EnquiryIn, db: Session = Depends(get_db), _: User = Depends(require_permission("enquiry.create"))):
    try:
        client = _sync_client_from_enquiry(db, payload)
        data = payload.model_dump(exclude={"company_name", "products"})
        data["client_id"] = client.id
        row = Enquiry(public_id=next_public_id(db, Enquiry, "ENQ"), **data)
        db.add(row)
        db.flush()
        _apply_enquiry_products(db, row, payload)
        db.commit()
    except IntegrityError as exc:
        db.rollback()
        raise HTTPException(status_code=400, detail="Unable to save enquiry. Please verify client and product details, then try again.") from exc
    row = db.query(Enquiry).options(selectinload(Enquiry.products)).filter(Enquiry.id == row.id).first()
    event = db.query(Event).filter(Event.enquiry_id == row.id).order_by(Event.id.desc()).first()
    return serialize_enquiry(row, event)


@router.patch("/enquiries/{enquiry_id}", response_model=EnquiryOut)
def update_enquiry(enquiry_id: int, payload: EnquiryIn, db: Session = Depends(get_db), _: User = Depends(require_permission("enquiry.edit"))):
    row = db.query(Enquiry).options(selectinload(Enquiry.products)).filter(Enquiry.id == enquiry_id).first()
    if row is None:
        raise HTTPException(status_code=404, detail="Enquiry not found")
    data = payload.model_dump(exclude={"company_name", "products"})
    for key, value in data.items():
        setattr(row, key, value)
    row.products.clear()
    db.flush()
    _apply_enquiry_products(db, row, payload)
    try:
        db.commit()
    except IntegrityError as exc:
        db.rollback()
        raise HTTPException(status_code=400, detail="Invalid product selection. Please refresh product list and try again.") from exc
    row = db.query(Enquiry).options(selectinload(Enquiry.products)).filter(Enquiry.id == row.id).first()
    event = db.query(Event).filter(Event.enquiry_id == row.id).order_by(Event.id.desc()).first()
    return serialize_enquiry(row, event)


@router.delete("/enquiries/{enquiry_id}")
def delete_enquiry(enquiry_id: int, db: Session = Depends(get_db), _: User = Depends(require_permission("enquiry.delete"))):
    row = db.query(Enquiry).filter(Enquiry.id == enquiry_id).first()
    if row is None:
        raise HTTPException(status_code=404, detail="Enquiry not found")
    if db.query(Event).filter(Event.enquiry_id == row.id).first() is not None:
        raise HTTPException(status_code=400, detail="Cannot delete enquiry that has been converted to an event")
    db.query(EnquiryFollowup).filter(EnquiryFollowup.enquiry_id == row.id).delete()
    for quote in db.query(Quotation).filter(Quotation.enquiry_id == row.id).all():
        quote.items.clear()
        db.delete(quote)
    db.delete(row)
    db.commit()
    return {"ok": True}


@router.get("/followups", response_model=list[FollowupOut])
def list_followups(
    status: str | None = None,
    enquiry_id: int | None = None,
    db: Session = Depends(get_db),
    _: User = Depends(require_permission("followup.view")),
):
    query = db.query(EnquiryFollowup)
    if status:
        query = query.filter(EnquiryFollowup.status == status)
    if enquiry_id is not None:
        query = query.filter(EnquiryFollowup.enquiry_id == enquiry_id)
    return query.order_by(EnquiryFollowup.followup_date.desc(), EnquiryFollowup.id.desc()).all()


@router.get("/followups/{followup_id}", response_model=FollowupOut)
def get_followup(followup_id: int, db: Session = Depends(get_db), _: User = Depends(require_permission("followup.view"))):
    row = db.query(EnquiryFollowup).filter(EnquiryFollowup.id == followup_id).first()
    if row is None:
        raise HTTPException(status_code=404, detail="Follow-up not found")
    return row


def _sync_enquiry_from_followup(db: Session, enquiry: Enquiry | None, followup: EnquiryFollowup) -> None:
    if enquiry is None or enquiry.status in {"Lost", "Cancelled"}:
        return
    if followup.status != "Completed":
        if enquiry.status == "New":
            enquiry.status = "Follow-up"
        return
    # Customer asked to follow up again — keep enquiry open and create the next touch.
    if followup.next_followup_date is not None:
        enquiry.status = "Follow-up"
        already = (
            db.query(EnquiryFollowup)
            .filter(
                EnquiryFollowup.enquiry_id == enquiry.id,
                EnquiryFollowup.followup_date == followup.next_followup_date,
                EnquiryFollowup.status == "Pending",
                EnquiryFollowup.id != followup.id,
            )
            .first()
        )
        if already is None:
            db.add(
                EnquiryFollowup(
                    enquiry_id=enquiry.id,
                    assigned_user_id=followup.assigned_user_id,
                    followup_date=followup.next_followup_date,
                    followup_type=followup.followup_type,
                    notes=None,
                    status="Pending",
                )
            )
        return
    enquiry.status = "Confirmed"


@router.post("/followups", response_model=FollowupOut)
def create_followup(payload: FollowupIn, db: Session = Depends(get_db), _: User = Depends(require_permission("followup.create"))):
    enquiry = db.query(Enquiry).filter(Enquiry.id == payload.enquiry_id).first()
    if enquiry is None:
        raise HTTPException(status_code=404, detail="Enquiry not found")
    row = EnquiryFollowup(**payload.model_dump())
    db.add(row)
    db.flush()
    _sync_enquiry_from_followup(db, enquiry, row)
    db.commit()
    db.refresh(row)
    return row


@router.patch("/followups/{followup_id}", response_model=FollowupOut)
def update_followup(followup_id: int, payload: FollowupIn, db: Session = Depends(get_db), _: User = Depends(require_permission("followup.edit"))):
    row = db.query(EnquiryFollowup).filter(EnquiryFollowup.id == followup_id).first()
    if row is None:
        raise HTTPException(status_code=404, detail="Follow-up not found")
    for key, value in payload.model_dump().items():
        setattr(row, key, value)
    enquiry = db.query(Enquiry).filter(Enquiry.id == row.enquiry_id).first()
    _sync_enquiry_from_followup(db, enquiry, row)
    db.commit()
    db.refresh(row)
    return row


def _next_quotation_number(db: Session) -> str:
    settings = db.query(QuotationSetting).first()
    if settings is None:
        settings = QuotationSetting(prefix="QUO", next_number=1)
        db.add(settings)
        db.flush()
    prefix = settings.prefix or "QUO"
    number = settings.next_number or 1
    value = f"{prefix}-{number:04d}"
    settings.next_number = number + 1
    return value


def _quotation_from_payload(db: Session, payload: QuotationIn, existing: Quotation | None = None) -> Quotation:
    row = existing or Quotation(public_id=_next_quotation_number(db))
    row.enquiry_id = payload.enquiry_id
    row.client_id = payload.client_id
    row.event_id = payload.event_id
    row.quotation_date = payload.quotation_date
    row.valid_until = payload.valid_until
    row.discount = money(payload.discount)
    row.terms = payload.terms
    row.status = payload.status
    if existing:
        row.items.clear()
        db.flush()
    items = []
    subtotal = money(0)
    for item in payload.items:
        amount = line_amount(item.quantity, item.unit_price, item.discount)
        subtotal += amount
        items.append(
            QuotationItem(
                name=item.name,
                product_id=item.product_id,
                quantity=item.quantity,
                unit_price=money(item.unit_price),
                discount=money(item.discount),
                amount=amount,
            )
        )
    row.items = items
    row.subtotal = subtotal
    transport, _ = _decode_transport(row.terms)
    row.final_amount = money(subtotal + transport - row.discount)
    if row.enquiry_id and row.status in {"Sent", "Accepted"}:
        enquiry = db.query(Enquiry).filter(Enquiry.id == row.enquiry_id).first()
        if enquiry:
            enquiry.status = "Quotation Sent" if row.status == "Sent" else "Confirmed"
    return row


@router.get("/quotations", response_model=list[QuotationOut])
def list_quotations(db: Session = Depends(get_db), _: User = Depends(require_permission("quotation.view"))):
    return db.query(Quotation).order_by(Quotation.id.desc()).all()


@router.get("/quotations/{quotation_id}", response_model=QuotationOut)
def get_quotation(quotation_id: int, db: Session = Depends(get_db), _: User = Depends(require_permission("quotation.view"))):
    row = db.query(Quotation).filter(Quotation.id == quotation_id).first()
    if row is None:
        raise HTTPException(status_code=404, detail="Quotation not found")
    return row


@router.post("/quotations", response_model=QuotationOut)
def create_quotation(payload: QuotationIn, db: Session = Depends(get_db), _: User = Depends(require_permission("quotation.create"))):
    row = _quotation_from_payload(db, payload)
    db.add(row)
    db.commit()
    db.refresh(row)
    return row


@router.patch("/quotations/{quotation_id}", response_model=QuotationOut)
def update_quotation(quotation_id: int, payload: QuotationIn, db: Session = Depends(get_db), _: User = Depends(require_permission("quotation.edit"))):
    row = db.query(Quotation).filter(Quotation.id == quotation_id).first()
    if row is None:
        raise HTTPException(status_code=404, detail="Quotation not found")
    _quotation_from_payload(db, payload, row)
    db.commit()
    db.refresh(row)
    return row


@router.post("/enquiries/{enquiry_id}/quotation", response_model=QuotationOut)
def create_quotation_from_enquiry(
    enquiry_id: int,
    payload: QuotationFromEnquiryIn,
    db: Session = Depends(get_db),
    _: User = Depends(require_permission("quotation.create")),
):
    enquiry = (
        db.query(Enquiry)
        .options(selectinload(Enquiry.products))
        .filter(Enquiry.id == enquiry_id)
        .first()
    )
    if enquiry is None:
        raise HTTPException(status_code=404, detail="Enquiry not found")
    if not enquiry.products:
        raise HTTPException(status_code=400, detail="Add products to the enquiry before sending a quotation")

    row = db.query(Quotation).filter(Quotation.enquiry_id == enquiry_id).first()
    if row is None:
        row = Quotation(public_id=_next_quotation_number(db))
        db.add(row)
    else:
        row.items.clear()
        db.flush()

    items = []
    subtotal = money(0)
    for product in enquiry.products:
        amount = line_amount(product.quantity, product.unit_price, product.discount)
        subtotal += amount
        items.append(
            QuotationItem(
                name=product.name,
                product_id=product.product_id,
                quantity=product.quantity,
                unit_price=money(product.unit_price),
                discount=money(product.discount),
                amount=amount,
            )
        )

    transport = money(payload.transport_amount)
    discount = money(payload.discount)
    row.enquiry_id = enquiry.id
    row.client_id = enquiry.client_id
    row.quotation_date = date.today()
    row.discount = discount
    row.terms = _encode_terms(transport, payload.terms)
    row.status = payload.status
    row.items = items
    row.subtotal = subtotal
    row.final_amount = money(subtotal + transport - discount)

    if row.status in {"Sent", "Accepted"} and enquiry.status not in {"Lost", "Cancelled", "Confirmed"}:
        enquiry.status = "Quotation Sent" if row.status == "Sent" else "Confirmed"

    db.commit()
    db.refresh(row)
    return row


@router.post("/quotations/{quotation_id}/mark-sent", response_model=QuotationOut)
def mark_quotation_sent(
    quotation_id: int,
    db: Session = Depends(get_db),
    _: User = Depends(require_permission("quotation.create")),
):
    row = db.query(Quotation).filter(Quotation.id == quotation_id).first()
    if row is None:
        raise HTTPException(status_code=404, detail="Quotation not found")
    row.status = "Sent"
    if row.enquiry_id:
        enquiry = db.query(Enquiry).filter(Enquiry.id == row.enquiry_id).first()
        if enquiry and enquiry.status not in {"Lost", "Cancelled", "Confirmed"}:
            enquiry.status = "Quotation Sent"
    db.commit()
    db.refresh(row)
    return row


@router.get("/quotations/{quotation_id}/pdf")
def download_quotation_pdf(
    quotation_id: int,
    db: Session = Depends(get_db),
    _: User = Depends(require_permission("quotation.view")),
):
    row = (
        db.query(Quotation)
        .options(selectinload(Quotation.items), selectinload(Quotation.client))
        .filter(Quotation.id == quotation_id)
        .first()
    )
    if row is None:
        raise HTTPException(status_code=404, detail="Quotation not found")
    if row.enquiry_id is None:
        raise HTTPException(status_code=400, detail="Quotation is not linked to an enquiry")

    enquiry = (
        db.query(Enquiry)
        .options(selectinload(Enquiry.products))
        .filter(Enquiry.id == row.enquiry_id)
        .first()
    )
    if enquiry is None:
        raise HTTPException(status_code=404, detail="Enquiry not found")

    product_ids = [item.product_id for item in row.items if item.product_id]
    products = db.query(Product).filter(Product.id.in_(product_ids or [0])).all()
    products_by_id = {product.id: product for product in products}
    company = db.query(CompanySetting).first()
    client = row.client or db.query(Client).filter(Client.id == row.client_id).first()
    transport, _ = _decode_transport(row.terms)
    sequence = _monthly_quotation_sequence(db, row.quotation_date)
    reference_no = quotation_reference(row, sequence)
    pdf_bytes = build_quotation_pdf(
        quotation=row,
        enquiry=enquiry,
        client=client,
        company=company,
        products_by_id=products_by_id,
        reference_no=reference_no,
        transport_amount=transport,
    )
    filename = f"{reference_no.replace('/', '-').replace(' ', '')}.pdf"
    return Response(
        content=pdf_bytes,
        media_type="application/pdf",
        headers={"Content-Disposition": f'attachment; filename="{filename}"'},
    )
