"""MCP server entry point.

Registers tools with FastMCP and runs on stdio so Claude Desktop / Cowork can
spawn it as a local MCP. Drop the launch command in claude_desktop_config.json:

    {
      "mcpServers": {
        "qb-distributor": {
          "command": "qb-distributor-mcp"
        }
      }
    }
"""
from __future__ import annotations

import functools
import logging
from typing import Any, Callable

from mcp.server.fastmcp import FastMCP

from . import tools
from .errors import FriendlyError, wrap_unknown

log = logging.getLogger(__name__)

mcp = FastMCP("qb-distributor")


def _friendly(fn: Callable[..., Any]) -> Callable[..., Any]:
    """Wrap a tool so any raised exception becomes a customer-friendly
    message with a link to the diagnose prompt instead of a raw traceback.

    FriendlyError subclasses already carry a formatted message — we just
    surface it. Everything else gets the generic SYMPTOM G wrapper.
    """

    @functools.wraps(fn)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        try:
            return fn(*args, **kwargs)
        except FriendlyError as exc:
            log.exception("Tool %s raised FriendlyError", fn.__name__)
            return {"error": str(exc)}
        except Exception as exc:  # noqa: BLE001 — intentional broad catch
            log.exception("Tool %s raised unexpected exception", fn.__name__)
            symptom = f"{fn.__name__} failed."
            # Recognize a few common substrings before falling back.
            msg = str(exc).lower()
            if "refresh" in msg and "token" in msg:
                from .errors import QuickBooksAuthFailed

                return {"error": str(QuickBooksAuthFailed(exc))}
            if "invalid_grant" in msg or "google_token" in msg or "google_client_secret" in msg:
                from .errors import GmailAuthFailed

                return {"error": str(GmailAuthFailed(exc))}
            if "numberformatexception" in msg or "customerref" in msg.replace(" ", ""):
                from .errors import CustomerNotLinked

                return {"error": str(CustomerNotLinked())}
            return {"error": wrap_unknown(symptom, exc)}

    return wrapper


@mcp.tool()
def list_incoming_pos(
    label: str | None = None,
    max_results: int = 20,
    unread_only: bool = True,
) -> dict[str, Any]:
    """List incoming purchase order emails from the configured Gmail label.

    Args:
        label: Gmail label to read. Defaults to GMAIL_PO_LABEL from .env.
        max_results: Max emails to return (default 20).
        unread_only: If True, only unread messages are returned.
    """
    return tools.list_incoming_pos(label=label, max_results=max_results, unread_only=unread_only)


@mcp.tool()
def parse_po_from_email(message_id: str) -> dict[str, Any]:
    """Fetch a PO email, pull its PDFs and body text, and return raw content plus
    any tabular line items the deterministic parser could extract.

    If `heuristic_lines` looks complete, pass it directly to `match_po_lines`.
    If it's empty or partial, read `raw_content` and build line items yourself
    before calling `match_po_lines`.
    """
    return tools.parse_po_from_email(message_id)


@mcp.tool()
def match_po_lines(
    lines: list[dict[str, Any]],
    customer_id: str | None = None,
    customer_name: str | None = None,
) -> dict[str, Any]:
    """Match PO line items against the QuickBooks catalog.

    Cascade: exact SKU → cross-reference table → fuzzy match (RapidFuzz). Lines
    below the confidence threshold are flagged needs_review with top candidates.

    Args:
        lines: List of POLine dicts (line_number, customer_part, quantity, etc.).
        customer_id: QB customer ID, if known.
        customer_name: Customer display name. Used to look up the QB customer
            and apply customer-specific cross-references.
    """
    return tools.match_po_lines(lines, customer_id=customer_id, customer_name=customer_name)


@mcp.tool()
def check_price_variance(
    matched_lines: list[dict[str, Any]],
    tolerance: float | None = None,
    customer_price_overrides: dict[str, str] | None = None,
) -> dict[str, Any]:
    """Compare PO prices against QB prices. Returns the lines that need a human
    decision (PO higher, PO lower, or missing on either side).

    Args:
        matched_lines: Output from `match_po_lines`.
        tolerance: Fraction; lines within ±tolerance are treated as 'match'.
        customer_price_overrides: {sku: price_string} — contract pricing wins
            over the catalog default when supplied.
    """
    return tools.check_price_variance(
        matched_lines,
        tolerance=tolerance,
        customer_price_overrides=customer_price_overrides,
    )


@mcp.tool()
def update_qb_item_price(qb_item_id: str, new_price: str) -> dict[str, Any]:
    """Update the UnitPrice on a QuickBooks item. The team should confirm before
    calling this — it writes to QB.
    """
    return tools.update_qb_item_price(qb_item_id, new_price)


@mcp.tool()
def propose_estimate(
    matched_lines: list[dict[str, Any]],
    customer_qb_id: str | None = None,
    customer_name: str | None = None,
    customer_po_number: str | None = None,
    message_id: str | None = None,
    include_unmatched: bool = False,
) -> dict[str, Any]:
    """Build an editable draft Estimate from match_po_lines output. Saves the
    draft locally — does NOT write to QuickBooks yet. Returns a draft_id the
    user can edit with update_draft_line / add_draft_line / etc., then push
    with submit_estimate_to_qb.
    """
    return tools.propose_estimate(
        matched_lines,
        customer_qb_id=customer_qb_id,
        customer_name=customer_name,
        customer_po_number=customer_po_number,
        message_id=message_id,
        include_unmatched=include_unmatched,
    )


@mcp.tool()
def get_draft(draft_id: str) -> dict[str, Any]:
    """Return the current state of a draft Estimate."""
    return tools.get_draft(draft_id)


@mcp.tool()
def list_drafts(status: str | None = "draft") -> dict[str, Any]:
    """List draft Estimates. status: 'draft' | 'submitted' | 'discarded' | None for all."""
    return tools.list_drafts(status)


@mcp.tool()
def update_draft_line(
    draft_id: str,
    line_id: str,
    quantity: str | None = None,
    unit_price: str | None = None,
    discount_pct: str | None = None,
    discount_amount: str | None = None,
    description: str | None = None,
    notes: str | None = None,
    qb_item_id: str | None = None,
    sku: str | None = None,
    name: str | None = None,
) -> dict[str, Any]:
    """Edit one line on a draft. Set quantity / unit_price to change the obvious
    things. Apply a line discount with either discount_pct (0-100) OR
    discount_amount in dollars. Reassign a SKU with qb_item_id / sku / name.
    """
    return tools.update_draft_line(
        draft_id, line_id,
        quantity=quantity, unit_price=unit_price,
        discount_pct=discount_pct, discount_amount=discount_amount,
        description=description, notes=notes,
        qb_item_id=qb_item_id, sku=sku, name=name,
    )


@mcp.tool()
def add_draft_line(
    draft_id: str,
    name: str,
    quantity: str = "1",
    unit_price: str = "0",
    description: str = "",
    qb_item_id: str | None = None,
    sku: str | None = None,
) -> dict[str, Any]:
    """Add a new line to a draft Estimate (e.g. freight, fee, manual SKU)."""
    return tools.add_draft_line(
        draft_id, name=name, quantity=quantity, unit_price=unit_price,
        description=description, qb_item_id=qb_item_id, sku=sku,
    )


@mcp.tool()
def remove_draft_line(draft_id: str, line_id: str) -> dict[str, Any]:
    """Drop a line from the draft."""
    return tools.remove_draft_line(draft_id, line_id)


@mcp.tool()
def set_draft_doc_discount(
    draft_id: str,
    discount_pct: str | None = None,
    discount_amount: str | None = None,
    freight: str | None = None,
    memo: str | None = None,
) -> dict[str, Any]:
    """Apply a document-level discount (pct OR amount, mutually exclusive),
    freight charge, or memo on a draft Estimate.
    """
    return tools.set_draft_doc_discount(
        draft_id, discount_pct=discount_pct, discount_amount=discount_amount,
        freight=freight, memo=memo,
    )


@mcp.tool()
def submit_estimate_to_qb(draft_id: str, confirm: bool = False) -> dict[str, Any]:
    """Push the draft Estimate to QuickBooks Online. Requires confirm=True.

    On success: creates the Estimate in QB, marks the draft as submitted,
    logs a usage event, and returns the QB doc number + URL.
    """
    return tools.submit_estimate_to_qb(draft_id, confirm=confirm)


@mcp.tool()
def discard_draft(draft_id: str) -> dict[str, Any]:
    """Mark a draft as discarded so it stops appearing in list_drafts."""
    return tools.discard_draft(draft_id)


@mcp.tool()
def license_status() -> dict[str, Any]:
    """Return current license tier, monthly quota, and usage."""
    return tools.license_status()


@mcp.tool()
def run_qb_report(report_name: str, params: dict[str, str] | None = None) -> dict[str, Any]:
    """Run a QuickBooks Online report. Useful names: ProfitAndLoss, CustomerSales,
    ItemSales, AgedReceivables, InventoryValuationSummary.
    """
    return tools.run_qb_report(report_name, params=params)


@mcp.tool()
def mark_email_processed(message_id: str) -> dict[str, Any]:
    """Move an email out of the unread queue and into the 'POs/Processed' label."""
    return tools.mark_email_processed(message_id)


@mcp.tool()
def refresh_catalog() -> dict[str, Any]:
    """Re-pull the QB item catalog. Call after adding new items or large price
    updates so subsequent matches see the latest data.
    """
    return tools.refresh_catalog()


def main() -> None:
    mcp.run()


if __name__ == "__main__":
    main()
