import json
import logging
import os
import uuid
from datetime import datetime
from decimal import Decimal, ROUND_HALF_UP
from pathlib import Path
from typing import Optional

from fastapi import HTTPException

from crm import nextcloud
from crm.quotation_models import (
    QuotationCreate,
    QuotationInDB,
    QuotationItemCreate,
    QuotationItemInDB,
    QuotationListItem,
    QuotationUpdate,
)
from crm.service import get_customer
import database as mqtt_db

logger = logging.getLogger(__name__)

# Path to Jinja2 templates directory (relative to this file)
_TEMPLATES_DIR = Path(__file__).parent.parent / "templates"


# ── Helpers ───────────────────────────────────────────────────────────────────

def _d(value) -> Decimal:
    """Convert to Decimal safely."""
    return Decimal(str(value if value is not None else 0))


def _float(d: Decimal) -> float:
    """Round Decimal to 2dp and return as float for storage."""
    return float(d.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))


def _calculate_totals(
    items: list,
    global_discount_percent: float,
    global_vat_percent: float,
    shipping_cost: float,
    shipping_cost_discount: float,
    install_cost: float,
    install_cost_discount: float,
    extras_cost: float,
) -> dict:
    """
    Calculate all monetary totals using Decimal arithmetic (ROUND_HALF_UP).
    VAT is a single global rate applied to items only (not shipping or install).
    Shipping and install costs carry 0% VAT.
    Returns a dict of floats ready for DB storage.
    """
    # Per-line totals (items only)
    item_totals = []
    for item in items:
        cost = _d(item.get("unit_cost", 0))
        qty = _d(item.get("quantity", 1))
        disc = _d(item.get("discount_percent", 0))
        net = cost * qty * (1 - disc / 100)
        item_totals.append(net)

    items_net = sum(item_totals, Decimal(0))

    # Shipping net (VAT = 0%)
    ship_gross = _d(shipping_cost)
    ship_disc = _d(shipping_cost_discount)
    ship_net = ship_gross * (1 - ship_disc / 100)

    # Install net (VAT = 0%)
    install_gross = _d(install_cost)
    install_disc = _d(install_cost_discount)
    install_net = install_gross * (1 - install_disc / 100)

    subtotal = items_net + ship_net + install_net

    global_disc_pct = _d(global_discount_percent)
    global_disc_amount = subtotal * (global_disc_pct / 100)
    new_subtotal = subtotal - global_disc_amount

    # VAT applies only to items portion, scaled by the global discount ratio
    vat_pct = _d(global_vat_percent)
    if subtotal > 0 and items_net > 0:
        items_ratio = items_net / subtotal
        vat_amount = new_subtotal * items_ratio * (vat_pct / 100)
    else:
        vat_amount = Decimal(0)

    extras = _d(extras_cost)
    final_total = new_subtotal + vat_amount + extras

    return {
        "subtotal_before_discount": _float(subtotal),
        "global_discount_amount": _float(global_disc_amount),
        "new_subtotal": _float(new_subtotal),
        "vat_amount": _float(vat_amount),
        "final_total": _float(final_total),
    }


def _calc_line_total(item) -> float:
    cost = _d(item.get("unit_cost", 0))
    qty = _d(item.get("quantity", 1))
    disc = _d(item.get("discount_percent", 0))
    return _float(cost * qty * (1 - disc / 100))


async def _generate_quotation_number(db) -> str:
    now = datetime.utcnow()
    yy = now.strftime("%y")
    mm = now.strftime("%m")
    prefix = f"QT-{yy}-{mm}-"
    rows = await db.execute_fetchall(
        "SELECT quotation_number FROM crm_quotations WHERE quotation_number LIKE ? ORDER BY quotation_number DESC LIMIT 1",
        (f"{prefix}%",),
    )
    if rows:
        last_num = rows[0][0]  # e.g. "QT-26-04-012"
        try:
            seq = int(last_num[len(prefix):]) + 1
        except ValueError:
            seq = 1
    else:
        seq = 1
    return f"{prefix}{seq:03d}"


def _row_to_quotation(row: dict, items: list[dict]) -> QuotationInDB:
    row = dict(row)
    row["comments"] = json.loads(row.get("comments") or "[]")
    row["quick_notes"] = json.loads(row.get("quick_notes") or "{}")
    item_models = [QuotationItemInDB(**{k: v for k, v in i.items() if k in QuotationItemInDB.model_fields}) for i in items]
    return QuotationInDB(**{k: v for k, v in row.items() if k in QuotationInDB.model_fields}, items=item_models)


def _row_to_list_item(row: dict) -> QuotationListItem:
    return QuotationListItem(**{k: v for k, v in dict(row).items() if k in QuotationListItem.model_fields})


async def _fetch_items(db, quotation_id: str) -> list[dict]:
    rows = await db.execute_fetchall(
        "SELECT * FROM crm_quotation_items WHERE quotation_id = ? ORDER BY sort_order ASC",
        (quotation_id,),
    )
    return [dict(r) for r in rows]


# ── Public API ────────────────────────────────────────────────────────────────

async def get_next_number() -> str:
    db = await mqtt_db.get_db()
    return await _generate_quotation_number(db)


async def list_all_quotations() -> list[dict]:
    """Return all quotations across all customers, with customer_name injected."""
    from shared.firebase import get_db as get_firestore
    db = await mqtt_db.get_db()
    rows = await db.execute_fetchall(
        "SELECT id, quotation_number, title, customer_id, status, final_total, created_at, updated_at, "
        "nextcloud_pdf_url, is_legacy, legacy_date, legacy_pdf_path "
        "FROM crm_quotations ORDER BY created_at DESC",
        (),
    )
    items = [dict(r) for r in rows]
    # Fetch unique customer names from Firestore in one pass
    customer_ids = {i["customer_id"] for i in items if i.get("customer_id")}
    customer_names: dict[str, str] = {}
    if customer_ids:
        fstore = get_firestore()
        for cid in customer_ids:
            try:
                doc = fstore.collection("crm_customers").document(cid).get()
                if doc.exists:
                    d = doc.to_dict()
                    name_parts = [d.get("name", ""), d.get("surname", "")]
                    full_name = " ".join(p for p in name_parts if p).strip()
                    org = (d.get("organization", "") or "").strip()
                    customer_names[cid] = {"name": full_name or cid, "org": org}
            except Exception:
                customer_names[cid] = {"name": cid, "org": ""}
    for item in items:
        info = customer_names.get(item["customer_id"], {"name": "", "org": ""})
        item["customer_name"] = info["name"]
        item["customer_org"] = info["org"]
    return items


async def list_quotations(customer_id: str) -> list[QuotationListItem]:
    db = await mqtt_db.get_db()
    rows = await db.execute_fetchall(
        "SELECT id, quotation_number, title, customer_id, status, final_total, created_at, updated_at, "
        "nextcloud_pdf_url, is_legacy, legacy_date, legacy_pdf_path "
        "FROM crm_quotations WHERE customer_id = ? ORDER BY created_at DESC",
        (customer_id,),
    )
    return [_row_to_list_item(dict(r)) for r in rows]


async def get_quotation(quotation_id: str) -> QuotationInDB:
    db = await mqtt_db.get_db()
    rows = await db.execute_fetchall(
        "SELECT * FROM crm_quotations WHERE id = ?", (quotation_id,)
    )
    if not rows:
        raise HTTPException(status_code=404, detail="Quotation not found")
    items = await _fetch_items(db, quotation_id)
    return _row_to_quotation(dict(rows[0]), items)


async def create_quotation(data: QuotationCreate, generate_pdf: bool = False) -> QuotationInDB:
    db = await mqtt_db.get_db()
    now = datetime.utcnow().isoformat()
    qid = str(uuid.uuid4())
    quotation_number = await _generate_quotation_number(db)

    # Build items list for calculation
    items_raw = [item.model_dump() for item in data.items]

    # Calculate per-item line totals
    for item in items_raw:
        item["line_total"] = _calc_line_total(item)

    totals = _calculate_totals(
        items_raw,
        data.global_discount_percent,
        data.global_vat_percent,
        data.shipping_cost,
        data.shipping_cost_discount,
        data.install_cost,
        data.install_cost_discount,
        data.extras_cost,
    )

    comments_json = json.dumps(data.comments)
    quick_notes_json = json.dumps(data.quick_notes or {})

    await db.execute(
        """INSERT INTO crm_quotations (
            id, quotation_number, title, subtitle, customer_id,
            language, status, order_type, shipping_method, estimated_shipping_date,
            global_discount_label, global_discount_percent, global_vat_percent,
            shipping_cost, shipping_cost_discount, install_cost, install_cost_discount,
            extras_label, extras_cost, comments, quick_notes,
            subtotal_before_discount, global_discount_amount, new_subtotal, vat_amount, final_total,
            nextcloud_pdf_path, nextcloud_pdf_url,
            client_org, client_name, client_location, client_phone, client_email,
            is_legacy, legacy_date, legacy_pdf_path,
            created_at, updated_at
        ) VALUES (
            ?, ?, ?, ?, ?,
            ?, 'draft', ?, ?, ?,
            ?, ?, ?,
            ?, ?, ?, ?,
            ?, ?, ?, ?,
            ?, ?, ?, ?, ?,
            NULL, NULL,
            ?, ?, ?, ?, ?,
            ?, ?, ?,
            ?, ?
        )""",
        (
            qid, quotation_number, data.title, data.subtitle, data.customer_id,
            data.language, data.order_type, data.shipping_method, data.estimated_shipping_date,
            data.global_discount_label, data.global_discount_percent, data.global_vat_percent,
            data.shipping_cost, data.shipping_cost_discount, data.install_cost, data.install_cost_discount,
            data.extras_label, data.extras_cost, comments_json, quick_notes_json,
            totals["subtotal_before_discount"], totals["global_discount_amount"],
            totals["new_subtotal"], totals["vat_amount"], totals["final_total"],
            data.client_org, data.client_name, data.client_location, data.client_phone, data.client_email,
            1 if data.is_legacy else 0, data.legacy_date, data.legacy_pdf_path,
            now, now,
        ),
    )

    # Insert items
    for i, item in enumerate(items_raw):
        item_id = str(uuid.uuid4())
        await db.execute(
            """INSERT INTO crm_quotation_items
               (id, quotation_id, product_id, description, description_en, description_gr,
                unit_type, unit_cost, discount_percent, quantity, vat_percent, line_total, sort_order)
               VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
            (
                item_id, qid, item.get("product_id"), item.get("description"),
                item.get("description_en"), item.get("description_gr"),
                item.get("unit_type", "pcs"), item.get("unit_cost", 0),
                item.get("discount_percent", 0), item.get("quantity", 1),
                item.get("vat_percent", 24), item["line_total"], item.get("sort_order", i),
            ),
        )

    await db.commit()

    quotation = await get_quotation(qid)

    if generate_pdf and not data.is_legacy:
        quotation = await _do_generate_and_upload_pdf(quotation)

    return quotation


async def update_quotation(quotation_id: str, data: QuotationUpdate, generate_pdf: bool = False) -> QuotationInDB:
    db = await mqtt_db.get_db()
    rows = await db.execute_fetchall(
        "SELECT * FROM crm_quotations WHERE id = ?", (quotation_id,)
    )
    if not rows:
        raise HTTPException(status_code=404, detail="Quotation not found")

    existing = dict(rows[0])
    now = datetime.utcnow().isoformat()

    # Merge update into existing values
    update_fields = data.model_dump(exclude_none=True)

    # Build SET clause — handle comments JSON separately
    set_parts = []
    params = []

    scalar_fields = [
        "title", "subtitle", "language", "status", "order_type", "shipping_method",
        "estimated_shipping_date", "global_discount_label", "global_discount_percent", "global_vat_percent",
        "shipping_cost", "shipping_cost_discount", "install_cost",
        "install_cost_discount", "extras_label", "extras_cost",
        "client_org", "client_name", "client_location", "client_phone", "client_email",
        "legacy_date", "legacy_pdf_path",
    ]

    for field in scalar_fields:
        if field in update_fields:
            set_parts.append(f"{field} = ?")
            params.append(update_fields[field])

    if "comments" in update_fields:
        set_parts.append("comments = ?")
        params.append(json.dumps(update_fields["comments"]))

    if "quick_notes" in update_fields:
        set_parts.append("quick_notes = ?")
        params.append(json.dumps(update_fields["quick_notes"] or {}))

    # Recalculate totals using merged values
    merged = {**existing, **{k: update_fields.get(k, existing.get(k)) for k in scalar_fields}}

    # If items are being updated, recalculate with new items; otherwise use existing items
    if "items" in update_fields:
        items_raw = [item.model_dump() for item in data.items]
        for item in items_raw:
            item["line_total"] = _calc_line_total(item)
    else:
        existing_items = await _fetch_items(db, quotation_id)
        items_raw = existing_items

    totals = _calculate_totals(
        items_raw,
        float(merged.get("global_discount_percent", 0)),
        float(merged.get("global_vat_percent", 24)),
        float(merged.get("shipping_cost", 0)),
        float(merged.get("shipping_cost_discount", 0)),
        float(merged.get("install_cost", 0)),
        float(merged.get("install_cost_discount", 0)),
        float(merged.get("extras_cost", 0)),
    )

    for field, val in totals.items():
        set_parts.append(f"{field} = ?")
        params.append(val)

    set_parts.append("updated_at = ?")
    params.append(now)
    params.append(quotation_id)

    if set_parts:
        await db.execute(
            f"UPDATE crm_quotations SET {', '.join(set_parts)} WHERE id = ?",
            params,
        )

    # Replace items if provided
    if "items" in update_fields:
        await db.execute("DELETE FROM crm_quotation_items WHERE quotation_id = ?", (quotation_id,))
        for i, item in enumerate(items_raw):
            item_id = str(uuid.uuid4())
            await db.execute(
                """INSERT INTO crm_quotation_items
                   (id, quotation_id, product_id, description, description_en, description_gr,
                    unit_type, unit_cost, discount_percent, quantity, vat_percent, line_total, sort_order)
                   VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
                (
                    item_id, quotation_id, item.get("product_id"), item.get("description"),
                    item.get("description_en"), item.get("description_gr"),
                    item.get("unit_type", "pcs"), item.get("unit_cost", 0),
                    item.get("discount_percent", 0), item.get("quantity", 1),
                    item.get("vat_percent", 24), item["line_total"], item.get("sort_order", i),
                ),
            )

    await db.commit()

    quotation = await get_quotation(quotation_id)

    if generate_pdf:
        quotation = await _do_generate_and_upload_pdf(quotation)

    return quotation


async def delete_quotation(quotation_id: str) -> None:
    db = await mqtt_db.get_db()
    rows = await db.execute_fetchall(
        "SELECT nextcloud_pdf_path FROM crm_quotations WHERE id = ?", (quotation_id,)
    )
    if not rows:
        raise HTTPException(status_code=404, detail="Quotation not found")

    pdf_path = dict(rows[0]).get("nextcloud_pdf_path")

    await db.execute("DELETE FROM crm_quotation_items WHERE quotation_id = ?", (quotation_id,))
    await db.execute("DELETE FROM crm_quotations WHERE id = ?", (quotation_id,))
    await db.commit()

    # Remove PDF from Nextcloud (best-effort)
    if pdf_path:
        try:
            await nextcloud.delete_file(pdf_path)
        except Exception as e:
            logger.warning("Failed to delete PDF from Nextcloud (%s): %s", pdf_path, e)


# ── PDF Generation ─────────────────────────────────────────────────────────────

async def _do_generate_and_upload_pdf(quotation: QuotationInDB) -> QuotationInDB:
    """Generate PDF, upload to Nextcloud, update DB record. Returns updated quotation."""
    try:
        customer = get_customer(quotation.customer_id)
    except Exception as e:
        logger.error("Cannot generate PDF — customer not found: %s", e)
        return quotation

    try:
        pdf_bytes = await _generate_pdf_bytes(quotation, customer)
    except Exception as e:
        logger.error("PDF generation failed for quotation %s: %s", quotation.id, e)
        return quotation

    # Delete old PDF if present
    if quotation.nextcloud_pdf_path:
        try:
            await nextcloud.delete_file(quotation.nextcloud_pdf_path)
        except Exception:
            pass

    try:
        pdf_path, pdf_url = await _upload_pdf(customer, quotation, pdf_bytes)
    except Exception as e:
        logger.error("PDF upload failed for quotation %s: %s", quotation.id, e)
        return quotation

    # Persist paths
    db = await mqtt_db.get_db()
    await db.execute(
        "UPDATE crm_quotations SET nextcloud_pdf_path = ?, nextcloud_pdf_url = ? WHERE id = ?",
        (pdf_path, pdf_url, quotation.id),
    )
    await db.commit()

    return await get_quotation(quotation.id)


async def _generate_pdf_bytes(quotation: QuotationInDB, customer) -> bytes:
    """Render Jinja2 template and convert to PDF via WeasyPrint."""
    from jinja2 import Environment, FileSystemLoader, select_autoescape
    import weasyprint

    env = Environment(
        loader=FileSystemLoader(str(_TEMPLATES_DIR)),
        autoescape=select_autoescape(["html"]),
    )

    def format_money(value):
        try:
            f = float(value)
            # Greek-style: dot thousands separator, comma decimal
            formatted = f"{f:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
            return f"{formatted} €"
        except (TypeError, ValueError):
            return "0,00 €"

    env.filters["format_money"] = format_money

    template = env.get_template("quotation.html")

    html_str = template.render(
        quotation=quotation,
        customer=customer,
        lang=quotation.language,
    )

    pdf = weasyprint.HTML(string=html_str, base_url=str(_TEMPLATES_DIR)).write_pdf()
    return pdf


async def _upload_pdf(customer, quotation: QuotationInDB, pdf_bytes: bytes) -> tuple[str, str]:
    """Upload PDF to Nextcloud, return (relative_path, public_url)."""
    from crm.service import get_customer_nc_path
    from config import settings

    nc_folder = get_customer_nc_path(customer)
    date_str = datetime.utcnow().strftime("%Y-%m-%d")
    filename = f"Quotation-{quotation.quotation_number}-{date_str}.pdf"
    rel_path = f"customers/{nc_folder}/quotations/{filename}"

    await nextcloud.ensure_folder(f"customers/{nc_folder}/quotations")
    await nextcloud.upload_file(rel_path, pdf_bytes, "application/pdf")

    # Construct a direct WebDAV download URL
    from crm.nextcloud import _full_url
    pdf_url = _full_url(rel_path)

    return rel_path, pdf_url


async def regenerate_pdf(quotation_id: str) -> QuotationInDB:
    quotation = await get_quotation(quotation_id)
    return await _do_generate_and_upload_pdf(quotation)


async def get_quotation_pdf_bytes(quotation_id: str) -> bytes:
    """Download the PDF for a quotation from Nextcloud and return raw bytes."""
    from fastapi import HTTPException
    quotation = await get_quotation(quotation_id)
    # For legacy quotations, the PDF is at legacy_pdf_path
    path = quotation.legacy_pdf_path if quotation.is_legacy else quotation.nextcloud_pdf_path
    if not path:
        raise HTTPException(status_code=404, detail="No PDF available for this quotation")
    pdf_bytes, _ = await nextcloud.download_file(path)
    return pdf_bytes


async def upload_legacy_pdf(quotation_id: str, pdf_bytes: bytes, filename: str) -> QuotationInDB:
    """Upload a legacy PDF to Nextcloud and store its path in the quotation record."""
    quotation = await get_quotation(quotation_id)
    if not quotation.is_legacy:
        raise HTTPException(status_code=400, detail="This quotation is not a legacy quotation")

    from crm.service import get_customer, get_customer_nc_path
    customer = get_customer(quotation.customer_id)
    nc_folder = get_customer_nc_path(customer)

    await nextcloud.ensure_folder(f"customers/{nc_folder}/quotations")
    rel_path = f"customers/{nc_folder}/quotations/{filename}"
    await nextcloud.upload_file(rel_path, pdf_bytes, "application/pdf")

    db = await mqtt_db.get_db()
    now = datetime.utcnow().isoformat()
    await db.execute(
        "UPDATE crm_quotations SET legacy_pdf_path = ?, updated_at = ? WHERE id = ?",
        (rel_path, now, quotation_id),
    )
    await db.commit()
    return await get_quotation(quotation_id)
