# ─── Document Template Renderer ────────────────────────────────────────────────
#
# Core engine for generating documents (invoices, quotes, receipts) from form
# submissions. Option C architecture: declarative JSON schema + style overrides.
#
# Pipeline:
#   submission → field_mapping → invoice_data → html_render → pdf_generate
#
# ─── Imports ───────────────────────────────────────────────────────────────────

import json
import logging
import os
import re
import uuid
from datetime import datetime, timezone
from pathlib import Path

from app.models import generate_invoice_number, update_next_invoice_number

logger = logging.getLogger("document")

# ─── Constants ─────────────────────────────────────────────────────────────────

DOCUMENT_TYPES = ["invoice", "quote", "receipt", "certificate", "report"]
TEMPLATE_LAYOUTS = [
    "stripe",     # minimalist, whitespace-driven, Stripe-inspired
    "hubspot",    # bold header band, color-blocked sections
    "notion",     # clean, editorial, sidebar accent
    "classic",    # traditional layout
    "modern",     # contemporary flex layout
    "professional",  # clean single-accent design
    "minimal",    # ultra-simple
    "bold",       # high-contrast
    "receipt",    # compact receipt format
    "customer_portal",  # customer-facing portal view
]
DOCUMENT_STATUSES = ["draft", "sent", "viewed", "paid", "void"]

# Where generated PDFs live on disk
PDF_STORAGE_DIR = Path("/app/data/documents")

# ─── Data Transformation ───────────────────────────────────────────────────────

def apply_field_mapping(submission: dict, field_mapping: dict) -> dict:
    """Transform raw submission data into document-ready structure.

    Takes a form submission and a field mapping config, producing a structured
    document data object. This is where free-form form data becomes invoice-ready.

    Args:
        submission: Raw submission dict from the database
        field_mapping: Mapping config defining how form fields → document fields

    Returns:
        Structured document data dict
    """
    data = submission.get("data", {})
    if not data:
        data = {}

    # ── Extract customer info from submission ──
    customer = {
        "name": submission.get("customer_name", ""),
        "email": submission.get("customer_email", ""),
        "phone": submission.get("customer_phone", ""),
    }

    # ── Process line items ──
    line_items = _build_line_items(data, field_mapping.get("line_items", []))

    # ── Calculate totals ──
    subtotal = sum(item.get("amount", 0) for item in line_items)
    tax_rate = field_mapping.get("tax_rate", 0.0)
    tax = round(subtotal * tax_rate, 2)
    total = round(subtotal + tax, 2)

    # ── Build document data ──
    document_data = {
        "from": {
            "name": field_mapping.get("business_name", "") or submission.get("business_name", ""),
            "email": field_mapping.get("business_email", "") or submission.get("business_email", ""),
            "phone": field_mapping.get("business_phone", "") or submission.get("business_phone", ""),
            "address": field_mapping.get("business_address", "") or submission.get("business_address", ""),
            "tax_id": field_mapping.get("tax_id", ""),
        },
        "to": customer,
        "line_items": line_items,
        "subtotal": subtotal,
        "tax_rate": tax_rate,
        "tax": tax,
        "total": total,
        "notes": field_mapping.get("notes", ""),
        "payment_terms": field_mapping.get("payment_terms", ""),
    }

    # ── Map custom fields ──
    for field_name, field_key in field_mapping.get("custom_fields", {}).items():
        document_data[field_name] = data.get(field_key, "")

    return document_data


def _build_line_items(data: dict, mapping: list) -> list:
    """Build line items from form data based on mapping config.

    Mapping format:
    [
        {"description": "field_key", "quantity": "field_key", "unit_price": "field_key"},
        {"description": "field_key", "quantity": "field_key", "unit_price": "field_key"},
    ]

    Each mapping entry produces one line item. Amount is calculated server-side.
    """
    line_items = []
    for item_map in mapping:
        description = data.get(item_map.get("description", ""), "")
        quantity = _safe_float(data.get(item_map.get("quantity", "1"), "1"))
        unit_price = _safe_float(data.get(item_map.get("unit_price", "0"), "0"))
        amount = round(quantity * unit_price, 2)

        if description or amount > 0:
            line_items.append({
                "description": description,
                "quantity": quantity,
                "unit_price": unit_price,
                "amount": amount,
            })

    return line_items


def _safe_float(value, default=0.0) -> float:
    """Safely parse a float from form data."""
    try:
        return float(value) if value is not None else default
    except (ValueError, TypeError):
        return default


# ─── HTML Rendering ────────────────────────────────────────────────────────────

def render_document_html(document_type: str, layout: str, data: dict,
                         style: dict = None) -> str:
    """Render document data as HTML using Jinja2 templates.

    Args:
        document_type: 'invoice', 'quote', 'receipt', etc.
        layout: 'classic', 'modern', 'receipt', 'statement'
        data: Structured document data from apply_field_mapping()
        style: Style override dict (colors, fonts, etc.)

    Returns:
        Rendered HTML string
    """
    from jinja2 import Environment, FileSystemLoader, select_autoescape

    # Resolve template directory
    template_dir = Path(__file__).parent.parent / "templates" / "documents"
    if not template_dir.exists():
        template_dir = Path(__file__).parent.parent / "templates"

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

    # Add utility filters
    env.filters["currency"] = _currency_filter
    env.filters["percent"] = _percent_filter
    env.filters["date_format"] = _date_filter
    env.filters["to_bool"] = lambda v: bool(v)
    env.filters["nl2br"] = lambda v: ("<br>\n").join(str(v).split("\n")) if v else ""

    # Merge default style with overrides
    style = {**DEFAULT_STYLE, **(style or {})}
    
    # Ensure discount field exists in data
    data.setdefault("discount", 0.0)

    # Determine template file
    template_name = f"{layout}.html"
    try:
        template = env.get_template(template_name)
    except Exception:
        # Fallback to classic layout
        template = env.get_template("classic.html")

    # Build render context
    context = {
        "document_type": document_type,
        "data": data,
        "style": style,
        "now": datetime.now(timezone.utc),
        "title_map": {
            "invoice": "INVOICE",
            "quote": "QUOTE",
            "receipt": "RECEIPT",
            "certificate": "CERTIFICATE",
            "report": "REPORT",
        },
    }

    return template.render(**context)


def _currency_filter(value) -> str:
    """Format value as currency string."""
    try:
        return f"${float(value):,.2f}"
    except (ValueError, TypeError):
        return "$0.00"


def _percent_filter(value) -> str:
    """Format value as percentage."""
    try:
        return f"{float(value) * 100:.1f}%"
    except (ValueError, TypeError):
        return "0.0%"


def _date_filter(value, fmt="%B %d, %Y") -> str:
    """Format date string."""
    if isinstance(value, str):
        try:
            return datetime.fromisoformat(value).strftime(fmt)
        except ValueError:
            return value
    return value if value else ""


def build_invoice_data(template: dict, submission: dict, line_items: list,
                       currency: str = "USD", doc_type: str = "Invoice") -> dict:
    """Build invoice data structure from template, submission, and line items.

    Returns a dict suitable for document rendering (HTML/PDF).
    """
    from datetime import datetime

    template_id = template.get("id")
    field_mapping = template.get("field_mapping", {})
    style_config = template.get("style_config", DEFAULT_STYLE)

    # Generate invoice number from template or default
    invoice_number = submission.get("invoice_number")
    if not invoice_number and template.get("next_invoice_number"):
        invoice_number = template["next_invoice_number"]
    if not invoice_number:
        invoice_number = f"INV-{datetime.now().strftime('%Y')}-{int(datetime.now().timestamp())}"

    # Generate document ID
    import uuid
    document_id = str(uuid.uuid4())[:8]

    # Build line items with calculations
    processed_items = []
    for item in line_items:
        qty = float(item.get("quantity", item.get("qty", 1)))
        price = float(item.get("unit_price", item.get("price", 0)))
        total = qty * price
        processed_items.append({
            "description": item.get("description", "Item"),
            "quantity": qty,
            "unit_price": price,
            "amount": total,
            "total": total,
        })

    # Calculate totals
    subtotal = sum(item["total"] for item in processed_items)
    tax_rate = float(submission.get("tax_rate", 0))
    tax_amount = subtotal * tax_rate / 100 if tax_rate else 0
    total = subtotal + tax_amount

    # Business info: check field_mapping first, then submission payload, then defaults
    from_info = {
        "name": field_mapping.get("business_name", "") or submission.get("business_name", ""),
        "email": field_mapping.get("business_email", "") or submission.get("business_email", ""),
        "phone": field_mapping.get("business_phone", "") or submission.get("business_phone", ""),
        "address": field_mapping.get("business_address", "") or submission.get("business_address", ""),
    }

    return {
        "document_id": document_id,
        "invoice_number": invoice_number,
        "document_type": doc_type,
        "currency": currency,
        "issue_date": datetime.now().strftime("%B %d, %Y"),
        "due_date": submission.get("due_date", ""),
        "from": from_info,
        "to": {
            "name": submission.get("customer_name", submission.get("name", "Customer")),
            "email": submission.get("customer_email", submission.get("email", "")),
            "address": submission.get("customer_address", submission.get("address", "")),
        },
        "customer_name": submission.get("customer_name", submission.get("name", "Customer")),
        "customer_email": submission.get("customer_email", submission.get("email", "")),
        "customer_address": submission.get("customer_address", submission.get("address", "")),
        "line_items": processed_items,
        "subtotal": subtotal,
        "tax_rate": tax_rate,
        "tax_amount": tax_amount,
        "tax": tax_amount,
        "total": total,
        "notes": submission.get("notes", ""),
        "payment_terms": submission.get("payment_terms", ""),
        "payment_link": submission.get("payment_link", ""),
        "payment_link_text": submission.get("payment_link_text", "Pay Now"),
        "style_config": style_config,
    }


# ─── Default Style ─────────────────────────────────────────────────────────────

DEFAULT_STYLE = {
    # ── Colors ──
    "primary_color": "#2563eb",
    "primary_dark": "#1D4ED8",
    "primary_light": "#DBEAFE",
    "text_primary": "#111827",
    "text_secondary": "#6B7280",
    "text_tertiary": "#9CA3AF",
    "border_color": "#F3F4F6",
    "bg_subtle": "#F9FAFB",
    # ── Typography ──
    "font_family": "Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
    "font_size": "11px",
    # ── Branding ──
    "logo_url": "",
    "show_watermark": False,
    "watermark_text": "PAID",
    # ── Page ──
    "page_format": "A4",
    "orientation": "portrait",
    "margin_top": "20mm",
    "margin_right": "20mm",
    "margin_bottom": "20mm",
    "margin_left": "20mm",
}


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

def generate_pdf(html_content: str, style: dict = None) -> bytes:
    """Generate PDF from HTML content using WeasyPrint.

    Args:
        html_content: Rendered HTML string (templates own their @page rules)
        style: Style config (passed to templates via render_document_html)

    Returns:
        PDF bytes

    Note: Templates define their own @page rules inline. We do NOT inject
    an external stylesheet here — that was overriding template @page/body
    rules and breaking full-bleed layouts (Stripe, HubSpot, Notion, etc.)
    which use @page { margin: 0 } with internal padding.
    """
    from weasyprint import HTML

    html_doc = HTML(string=html_content, base_url=str(Path(__file__).parent.parent))
    return html_doc.write_pdf()


# ─── Full Pipeline ─────────────────────────────────────────────────────────────

def render_document(template_id: int, submission_id: int,
                    template_data: dict, submission_data: dict,
                    style: dict = None) -> dict:
    """Full document generation pipeline.

    Takes a template and submission, produces a document with PDF.

    Args:
        template_id: Document template ID
        submission_id: Form submission ID
        template_data: Template config from DB (layout, field_mapping, etc.)
        submission_data: Raw submission from DB
        style: Style overrides

    Returns:
        {
            "document_id": str,
            "pdf_bytes": bytes,
            "html_preview": str,
            "data": dict,          # structured document data
            "size_bytes": int,
        }
    """
    # Step 1: Transform submission data
    field_mapping = template_data.get("field_mapping")
    if isinstance(field_mapping, str):
        field_mapping = json.loads(field_mapping)
    elif field_mapping is None:
        field_mapping = {}
    document_data = apply_field_mapping(submission_data, field_mapping)

    # Step 2: Generate document_id early (needed for payment link)
    document_id = str(uuid.uuid4())

    # Step 3: Add document metadata
    document_data["document_number"] = field_mapping.get("document_number", "")
    document_data["issue_date"] = submission_data.get("submitted_at", "")
    document_data["due_date"] = field_mapping.get("due_date", "")

    # Build payment link — auto-generate Stripe link if Stripe is configured
    payment_link = field_mapping.get("payment_link", "")
    if not payment_link:
        app_url = os.environ.get("APP_URL", "https://agentforms.io")
        payment_link = f"{app_url}/documents/{document_id}/pay"
    document_data["payment_link"] = payment_link
    document_data["payment_link_text"] = field_mapping.get("payment_link_text", "Pay Now")

    # Step 2b: Generate invoice number for invoice documents
    invoice_number = None
    if template_data.get("document_type") == "invoice" and template_id:
        try:
            invoice_number = generate_invoice_number(template_id)
            document_data["document_number"] = invoice_number
            # Advance the next_invoice_number for the template
            current = invoice_number
            match = re.search(r'(\d+)$', current)
            if match:
                seq = int(match.group(1))
                seq_len = len(match.group(1))
                next_num = current[:match.start()] + str(seq + 1).zfill(seq_len)
                update_next_invoice_number(template_id, next_num)
        except Exception as e:
            logger.warning("invoice_number_gen", f"Failed to generate invoice number: {e}")

    # Step 3: Render HTML
    layout = template_data.get("layout", "classic")
    doc_type = template_data.get("document_type", "invoice")
    html_content = render_document_html(doc_type, layout, document_data, style)

    # Step 4: Generate PDF
    pdf_bytes = generate_pdf(html_content, style)

    return {
        "document_id": document_id,
        "pdf_bytes": pdf_bytes,
        "html_preview": html_content,
        "data": document_data,
        "size_bytes": len(pdf_bytes),
        "invoice_number": invoice_number,
    }


# ─── PDF Storage ───────────────────────────────────────────────────────────────

def save_pdf(pdf_bytes: bytes, document_id: str) -> str:
    """Save PDF bytes to disk. Returns relative path."""
    PDF_STORAGE_DIR.mkdir(parents=True, exist_ok=True)
    filename = f"{document_id}.pdf"
    filepath = PDF_STORAGE_DIR / filename
    filepath.write_bytes(pdf_bytes)
    return str(filepath)


def get_pdf_path(document_id: str) -> Path:
    """Get absolute path for a stored PDF."""
    return PDF_STORAGE_DIR / f"{document_id}.pdf"


def render_document_pdf(invoice_data: dict, template: dict, document_type: str = "invoice") -> str:
    """Render a document to PDF from pre-built invoice data.

    Returns the relative PDF storage path.
    """
    import uuid
    document_id = invoice_data.get("document_id") or str(uuid.uuid4())[:12]
    layout = template.get("layout", "classic")
    style = template.get("style_config") or {}
    html_content = render_document_html(document_type, layout, invoice_data, style)
    pdf_bytes = generate_pdf(html_content, style)
    pdf_path = save_pdf(pdf_bytes, document_id)
    return pdf_path


# ─── Recurring Schedule Helpers ────────────────────────────────────────────────+
def calculate_next_run(current_run: str, interval: str, interval_count: int = 1) -> str:
    """Calculate the next run date for a recurring schedule.

    Args:
        current_run: ISO date string (YYYY-MM-DD)
        interval: 'daily', 'weekly', 'monthly', 'yearly'
        interval_count: Number of intervals to advance (default 1)
    """
    from dateutil.relativedelta import relativedelta

    try:
        current = datetime.fromisoformat(current_run).date()
    except (ValueError, TypeError):
        current = datetime.now(timezone.utc).date()

    if interval == "daily":
        delta = relativedelta(days=interval_count)
    elif interval == "weekly":
        delta = relativedelta(weeks=interval_count)
    elif interval == "monthly":
        delta = relativedelta(months=interval_count)
    elif interval == "yearly":
        delta = relativedelta(years=interval_count)
    else:
        delta = relativedelta(months=interval_count)  # default to monthly

    return (current + delta).isoformat()

