"""QuickBooks Online API client.

Pure Python implementation - no Django dependencies.
Token management and storage is handled by the consuming application.
"""

from __future__ import annotations

import logging
from dataclasses import dataclass
from datetime import date, datetime, timedelta
from decimal import Decimal
from typing import Any, Callable

logger = logging.getLogger(__name__)


@dataclass
class QuickBooksConfig:
    """Configuration for QuickBooks client."""

    client_id: str
    client_secret: str
    redirect_uri: str
    access_token: str
    refresh_token: str
    realm_id: str
    sandbox: bool = True
    access_token_expires_at: datetime | None = None
    on_token_refresh: Callable[[str, str, datetime], None] | None = None


class QuickBooksClientError(Exception):
    """Base exception for QuickBooks client errors."""

    pass


class TokenExpiredError(QuickBooksClientError):
    """Raised when tokens are expired and cannot be refreshed."""

    pass


class QuickBooksClient:
    """QuickBooks Online API client with automatic token refresh.

    This is a pure Python client with no Django dependencies.
    Token storage callbacks allow integration with any storage backend.

    Example:
        def save_tokens(access_token, refresh_token, expires_at):
            # Save to your database
            connection.access_token = access_token
            connection.refresh_token = refresh_token
            connection.access_token_expires_at = expires_at
            connection.save()

        client = QuickBooksClient(
            config=QuickBooksConfig(
                client_id="...",
                client_secret="...",
                redirect_uri="...",
                access_token=connection.access_token,
                refresh_token=connection.refresh_token,
                realm_id=connection.realm_id,
                access_token_expires_at=connection.access_token_expires_at,
                on_token_refresh=save_tokens,
            )
        )
    """

    def __init__(self, config: QuickBooksConfig):
        """Initialize client with configuration.

        Args:
            config: QuickBooksConfig with credentials and tokens
        """
        self.config = config
        self._qb_client = None
        self._auth_client = None

    @property
    def api_base_url(self) -> str:
        """Get the API base URL based on environment."""
        if self.config.sandbox:
            return "https://sandbox-quickbooks.api.intuit.com"
        return "https://quickbooks.api.intuit.com"

    def _is_token_expired(self) -> bool:
        """Check if access token is expired or about to expire."""
        if not self.config.access_token_expires_at:
            return True
        buffer = timedelta(minutes=5)
        return datetime.utcnow() >= (self.config.access_token_expires_at - buffer)

    def _refresh_tokens_if_needed(self) -> bool:
        """Refresh OAuth tokens if expired.

        Returns:
            True if tokens were refreshed, False if still valid
        """
        if not self._is_token_expired():
            return False

        logger.info(f"Refreshing QuickBooks tokens for realm {self.config.realm_id}")

        from lightwave.integrations.quickbooks.oauth import refresh_access_token

        try:
            token_response = refresh_access_token(
                refresh_token=self.config.refresh_token,
                client_id=self.config.client_id,
                client_secret=self.config.client_secret,
                redirect_uri=self.config.redirect_uri,
                sandbox=self.config.sandbox,
            )

            # Update config
            self.config.access_token = token_response.access_token
            self.config.refresh_token = token_response.refresh_token
            self.config.access_token_expires_at = token_response.access_token_expires_at

            # Notify callback if provided
            if self.config.on_token_refresh:
                self.config.on_token_refresh(
                    token_response.access_token,
                    token_response.refresh_token,
                    token_response.access_token_expires_at,
                )

            # Reset cached client
            self._qb_client = None
            self._auth_client = None

            logger.info(f"Successfully refreshed tokens for realm {self.config.realm_id}")
            return True

        except Exception as e:
            logger.error(f"Failed to refresh QuickBooks tokens: {e}")
            raise QuickBooksClientError(f"Failed to refresh tokens: {e}") from e

    def _get_auth_client(self):
        """Get or create the OAuth client."""
        if self._auth_client is None:
            try:
                from intuitlib.client import AuthClient

                self._auth_client = AuthClient(
                    client_id=self.config.client_id,
                    client_secret=self.config.client_secret,
                    redirect_uri=self.config.redirect_uri,
                    environment="sandbox" if self.config.sandbox else "production",
                )
                self._auth_client.access_token = self.config.access_token
            except ImportError as e:
                raise QuickBooksClientError("intuit-oauth not installed. Install with: pip install intuit-oauth") from e
        return self._auth_client

    def _get_qb_client(self):
        """Get or create the QuickBooks API client."""
        if self._qb_client is None:
            try:
                from quickbooks import QuickBooks

                self._refresh_tokens_if_needed()

                auth_client = self._get_auth_client()
                self._qb_client = QuickBooks(
                    auth_client=auth_client,
                    refresh_token=self.config.refresh_token,
                    company_id=self.config.realm_id,
                    minorversion=65,
                )
            except ImportError as e:
                raise QuickBooksClientError(
                    "python-quickbooks not installed. Install with: pip install python-quickbooks"
                ) from e
        return self._qb_client

    # =========================================================================
    # Customer Operations
    # =========================================================================

    def get_customers(self, active_only: bool = True) -> list[dict[str, Any]]:
        """Fetch all customers from QuickBooks.

        Args:
            active_only: If True, only return active customers

        Returns:
            List of customer dictionaries
        """
        from quickbooks.objects.customer import Customer

        client = self._get_qb_client()

        if active_only:
            customers = Customer.filter(Active=True, qb=client)
        else:
            customers = Customer.all(qb=client)

        return [self._customer_to_dict(c) for c in customers]

    def get_customer(self, customer_id: str) -> dict[str, Any] | None:
        """Fetch a single customer by ID."""
        from quickbooks.objects.customer import Customer

        client = self._get_qb_client()

        try:
            customer = Customer.get(customer_id, qb=client)
            return self._customer_to_dict(customer)
        except Exception as e:
            logger.error(f"Failed to get customer {customer_id}: {e}")
            return None

    def create_customer(
        self,
        display_name: str,
        company_name: str | None = None,
        email: str | None = None,
        phone: str | None = None,
        billing_address: dict[str, str] | None = None,
    ) -> dict[str, Any]:
        """Create a new customer in QuickBooks.

        Args:
            display_name: Customer display name (required, must be unique)
            company_name: Company name
            email: Email address
            phone: Phone number
            billing_address: Dict with keys: line1, city, country_sub_division_code, postal_code, country

        Returns:
            Created customer dictionary
        """
        from quickbooks.objects.customer import Customer

        client = self._get_qb_client()

        customer = Customer()
        customer.DisplayName = display_name

        if company_name:
            customer.CompanyName = company_name

        if email:
            from quickbooks.objects.base import EmailAddress

            customer.PrimaryEmailAddr = EmailAddress()
            customer.PrimaryEmailAddr.Address = email

        if phone:
            from quickbooks.objects.base import PhoneNumber

            customer.PrimaryPhone = PhoneNumber()
            customer.PrimaryPhone.FreeFormNumber = phone

        if billing_address:
            from quickbooks.objects.base import Address

            addr = Address()
            addr.Line1 = billing_address.get("line1", "")
            addr.City = billing_address.get("city", "")
            addr.CountrySubDivisionCode = billing_address.get("country_sub_division_code", "")
            addr.PostalCode = billing_address.get("postal_code", "")
            addr.Country = billing_address.get("country", "")
            customer.BillAddr = addr

        customer.save(qb=client)
        logger.info(f"Created customer {customer.Id}: {display_name}")

        return self._customer_to_dict(customer)

    def _customer_to_dict(self, customer) -> dict[str, Any]:
        """Convert QuickBooks Customer object to dictionary."""
        return {
            "id": customer.Id,
            "display_name": customer.DisplayName,
            "company_name": customer.CompanyName,
            "given_name": customer.GivenName,
            "family_name": customer.FamilyName,
            "email": customer.PrimaryEmailAddr.Address if customer.PrimaryEmailAddr else None,
            "phone": customer.PrimaryPhone.FreeFormNumber if customer.PrimaryPhone else None,
            "balance": float(customer.Balance) if customer.Balance else 0.0,
            "active": customer.Active,
        }

    # =========================================================================
    # Invoice Operations
    # =========================================================================

    def get_invoices(
        self,
        status: str | None = None,
        start_date: str | None = None,
        end_date: str | None = None,
        customer_id: str | None = None,
    ) -> list[dict[str, Any]]:
        """Fetch invoices from QuickBooks.

        Args:
            status: Filter by status ('Open', 'Paid', 'Overdue')
            start_date: Filter by TxnDate >= (YYYY-MM-DD)
            end_date: Filter by TxnDate <= (YYYY-MM-DD)
            customer_id: Filter by customer ID

        Returns:
            List of invoice dictionaries
        """
        from quickbooks.objects.invoice import Invoice

        client = self._get_qb_client()

        # Build query
        query = "SELECT * FROM Invoice"
        conditions = []

        if start_date:
            conditions.append(f"TxnDate >= '{start_date}'")
        if end_date:
            conditions.append(f"TxnDate <= '{end_date}'")
        if customer_id:
            conditions.append(f"CustomerRef = '{customer_id}'")

        if conditions:
            query += " WHERE " + " AND ".join(conditions)

        query += " ORDERBY TxnDate DESC"

        invoices = Invoice.query(query, qb=client)
        result = [self._invoice_to_dict(inv) for inv in invoices]

        # Filter by status if specified
        if status:
            status_lower = status.lower()
            result = [inv for inv in result if inv.get("status", "").lower() == status_lower]

        return result

    def create_invoice(
        self,
        customer_id: str,
        line_items: list[dict[str, Any]],
        due_date: str | None = None,
        memo: str | None = None,
        doc_number: str | None = None,
    ) -> dict[str, Any]:
        """Create a new invoice in QuickBooks.

        Args:
            customer_id: QuickBooks customer ID
            line_items: List of line items, each with:
                - description: str
                - amount: float
                - quantity: int (default 1)
            due_date: Due date (YYYY-MM-DD)
            memo: Private memo for the invoice
            doc_number: Custom invoice number

        Returns:
            Created invoice dictionary
        """
        from quickbooks.objects.customer import Customer
        from quickbooks.objects.detailline import SalesItemLine, SalesItemLineDetail
        from quickbooks.objects.invoice import Invoice

        client = self._get_qb_client()

        invoice = Invoice()

        # Set customer reference
        customer_ref = Customer.get(customer_id, qb=client)
        invoice.CustomerRef = customer_ref.to_ref()

        # Add line items
        for i, item in enumerate(line_items, start=1):
            line = SalesItemLine()
            line.LineNum = i
            line.Amount = item.get("amount", 0)
            line.Description = item.get("description", "")
            line.DetailType = "SalesItemLineDetail"

            detail = SalesItemLineDetail()
            qty = item.get("quantity", 1)
            detail.Qty = qty
            detail.UnitPrice = item.get("amount", 0) / qty if qty else 0

            line.SalesItemLineDetail = detail
            invoice.Line.append(line)

        if due_date:
            invoice.DueDate = due_date

        if memo:
            invoice.PrivateNote = memo

        if doc_number:
            invoice.DocNumber = doc_number

        invoice.save(qb=client)
        logger.info(f"Created invoice {invoice.Id} for customer {customer_id}")

        return self._invoice_to_dict(invoice)

    def _invoice_to_dict(self, invoice) -> dict[str, Any]:
        """Convert QuickBooks Invoice object to dictionary."""
        balance = Decimal(str(invoice.Balance)) if invoice.Balance else Decimal("0")
        total = Decimal(str(invoice.TotalAmt)) if invoice.TotalAmt else Decimal("0")

        # Determine status
        if balance == 0 and total > 0:
            status = "Paid"
        elif invoice.DueDate:
            due = date.fromisoformat(invoice.DueDate) if isinstance(invoice.DueDate, str) else invoice.DueDate
            if due < date.today() and balance > 0:
                status = "Overdue"
            else:
                status = "Open"
        else:
            status = "Open"

        return {
            "id": invoice.Id,
            "doc_number": invoice.DocNumber,
            "customer_id": invoice.CustomerRef.value if invoice.CustomerRef else None,
            "customer_name": invoice.CustomerRef.name if invoice.CustomerRef else None,
            "txn_date": invoice.TxnDate,
            "due_date": invoice.DueDate,
            "total": float(total),
            "balance": float(balance),
            "status": status,
            "email_status": invoice.EmailStatus,
            "line_items": [
                {
                    "description": line.Description,
                    "amount": float(line.Amount) if line.Amount else 0,
                }
                for line in (invoice.Line or [])
                if hasattr(line, "Description")
            ],
        }

    # =========================================================================
    # Account Operations
    # =========================================================================

    def get_accounts(self, account_type: str | None = None) -> list[dict[str, Any]]:
        """Fetch chart of accounts from QuickBooks.

        Args:
            account_type: Filter by account type (e.g., 'Bank', 'Income', 'Expense')

        Returns:
            List of account dictionaries
        """
        from quickbooks.objects.account import Account

        client = self._get_qb_client()

        if account_type:
            accounts = Account.filter(AccountType=account_type, qb=client)
        else:
            accounts = Account.all(qb=client)

        return [
            {
                "id": acc.Id,
                "name": acc.Name,
                "full_name": acc.FullyQualifiedName,
                "account_type": acc.AccountType,
                "account_sub_type": acc.AccountSubType,
                "current_balance": float(acc.CurrentBalance) if acc.CurrentBalance else 0.0,
                "active": acc.Active,
            }
            for acc in accounts
        ]

    # =========================================================================
    # Vendor Operations
    # =========================================================================

    def get_vendors(self, active_only: bool = True) -> list[dict[str, Any]]:
        """Fetch all vendors from QuickBooks.

        Args:
            active_only: If True, only return active vendors

        Returns:
            List of vendor dictionaries
        """
        from quickbooks.objects.vendor import Vendor

        client = self._get_qb_client()

        if active_only:
            vendors = Vendor.filter(Active=True, qb=client)
        else:
            vendors = Vendor.all(qb=client)

        return [self._vendor_to_dict(v) for v in vendors]

    def get_vendor(self, vendor_id: str) -> dict[str, Any] | None:
        """Fetch a single vendor by ID."""
        from quickbooks.objects.vendor import Vendor

        client = self._get_qb_client()

        try:
            vendor = Vendor.get(vendor_id, qb=client)
            return self._vendor_to_dict(vendor)
        except Exception as e:
            logger.error(f"Failed to get vendor {vendor_id}: {e}")
            return None

    def _vendor_to_dict(self, vendor) -> dict[str, Any]:
        """Convert QuickBooks Vendor object to dictionary."""
        return {
            "id": vendor.Id,
            "display_name": vendor.DisplayName,
            "company_name": vendor.CompanyName,
            "given_name": vendor.GivenName,
            "family_name": vendor.FamilyName,
            "email": vendor.PrimaryEmailAddr.Address if vendor.PrimaryEmailAddr else None,
            "phone": vendor.PrimaryPhone.FreeFormNumber if vendor.PrimaryPhone else None,
            "balance": float(vendor.Balance) if vendor.Balance else 0.0,
            "active": vendor.Active,
            "vendor_1099": getattr(vendor, "Vendor1099", False),
            "tax_identifier": getattr(vendor, "TaxIdentifier", None),
        }

    # =========================================================================
    # Bill Operations
    # =========================================================================

    def get_bills(
        self,
        status: str | None = None,
        start_date: str | None = None,
        end_date: str | None = None,
        vendor_id: str | None = None,
    ) -> list[dict[str, Any]]:
        """Fetch bills (payables) from QuickBooks.

        Args:
            status: Filter by status ('Open', 'Paid', 'Overdue')
            start_date: Filter by TxnDate >= (YYYY-MM-DD)
            end_date: Filter by TxnDate <= (YYYY-MM-DD)
            vendor_id: Filter by vendor ID

        Returns:
            List of bill dictionaries
        """
        from quickbooks.objects.bill import Bill

        client = self._get_qb_client()

        # Build query
        query = "SELECT * FROM Bill"
        conditions = []

        if start_date:
            conditions.append(f"TxnDate >= '{start_date}'")
        if end_date:
            conditions.append(f"TxnDate <= '{end_date}'")
        if vendor_id:
            conditions.append(f"VendorRef = '{vendor_id}'")

        if conditions:
            query += " WHERE " + " AND ".join(conditions)

        query += " ORDERBY TxnDate DESC"

        bills = Bill.query(query, qb=client)
        result = [self._bill_to_dict(bill) for bill in bills]

        # Filter by status if specified
        if status:
            status_lower = status.lower()
            result = [b for b in result if b.get("status", "").lower() == status_lower]

        return result

    def _bill_to_dict(self, bill) -> dict[str, Any]:
        """Convert QuickBooks Bill object to dictionary."""
        balance = Decimal(str(bill.Balance)) if bill.Balance else Decimal("0")
        total = Decimal(str(bill.TotalAmt)) if bill.TotalAmt else Decimal("0")

        # Determine status
        if balance == 0 and total > 0:
            status = "Paid"
        elif bill.DueDate:
            due = date.fromisoformat(bill.DueDate) if isinstance(bill.DueDate, str) else bill.DueDate
            if due < date.today() and balance > 0:
                status = "Overdue"
            else:
                status = "Open"
        else:
            status = "Open"

        return {
            "id": bill.Id,
            "doc_number": getattr(bill, "DocNumber", None),
            "vendor_id": bill.VendorRef.value if bill.VendorRef else None,
            "vendor_name": bill.VendorRef.name if bill.VendorRef else None,
            "txn_date": bill.TxnDate,
            "due_date": bill.DueDate,
            "total": float(total),
            "balance": float(balance),
            "status": status,
            "ap_account": bill.APAccountRef.name if bill.APAccountRef else None,
            "line_items": [
                {
                    "description": getattr(line, "Description", ""),
                    "amount": float(line.Amount) if line.Amount else 0,
                    "account": line.AccountBasedExpenseLineDetail.AccountRef.name
                    if hasattr(line, "AccountBasedExpenseLineDetail")
                    and line.AccountBasedExpenseLineDetail
                    and line.AccountBasedExpenseLineDetail.AccountRef
                    else None,
                }
                for line in (bill.Line or [])
                if hasattr(line, "Amount")
            ],
        }

    # =========================================================================
    # Reports
    # =========================================================================

    def get_profit_loss(self, start_date: str, end_date: str) -> dict[str, Any]:
        """Fetch Profit & Loss report from QuickBooks."""
        from quickbooks.objects.report import Report

        client = self._get_qb_client()
        report = Report.get(
            "ProfitAndLoss",
            qb=client,
            params={"start_date": start_date, "end_date": end_date},
        )
        return self._report_to_dict(report)

    def get_balance_sheet(self, as_of_date: str | None = None) -> dict[str, Any]:
        """Fetch Balance Sheet report from QuickBooks."""
        from quickbooks.objects.report import Report

        client = self._get_qb_client()
        params = {}
        if as_of_date:
            params["end_date"] = as_of_date

        report = Report.get("BalanceSheet", qb=client, params=params)
        return self._report_to_dict(report)

    def get_ar_aging(self) -> dict[str, Any]:
        """Fetch Accounts Receivable Aging report."""
        from quickbooks.objects.report import Report

        client = self._get_qb_client()
        report = Report.get("AgedReceivables", qb=client, params={"aging_method": "Report_Date"})
        return self._report_to_dict(report)

    def _report_to_dict(self, report) -> dict[str, Any]:
        """Convert QuickBooks Report object to dictionary."""
        return {
            "name": getattr(report, "ReportName", "Report"),
            "basis": getattr(report, "ReportBasis", None),
            "start_period": getattr(report, "StartPeriod", None),
            "end_period": getattr(report, "EndPeriod", None),
            "currency": getattr(report, "Currency", "USD"),
            "rows": getattr(report, "Rows", []),
            "columns": getattr(report, "Columns", []),
        }

    # =========================================================================
    # Company Info
    # =========================================================================

    def get_company_info(self) -> dict[str, Any]:
        """Fetch company information from QuickBooks."""
        from quickbooks.objects.companyinfo import CompanyInfo

        client = self._get_qb_client()
        info = CompanyInfo.get(self.config.realm_id, qb=client)

        return {
            "id": info.Id,
            "company_name": info.CompanyName,
            "legal_name": info.LegalName,
            "email": info.Email.Address if info.Email else None,
            "phone": info.PrimaryPhone.FreeFormNumber if info.PrimaryPhone else None,
            "country": info.Country,
            "fiscal_year_start": info.FiscalYearStartMonth,
        }
