"""
AI assistant integration using MiniMax chat completions.
"""

import asyncio
import json
import re
from typing import Optional

import httpx

from app.core.config import get_ai_config

MINIMAX_CHAT_COMPLETIONS_URL = "https://api.minimax.io/v1/chat/completions"
MINIMAX_TOKEN_PLAN_CHAT_COMPLETIONS_URL = "https://api.minimaxi.com/v1/chat/completions"
DEFAULT_MINIMAX_MODEL = "MiniMax-M2.7"
RECEIPT_EXTRACTION_PROMPT = """You extract structured data from New Zealand expense receipts and supplier invoices.

You receive OCR text from one or more pages of a single uploaded receipt document.
Return JSON only, no markdown, no explanation.

Required shape:
{
  "vendor_name": "",
  "invoice_number": "",
  "invoice_date": "YYYY-MM-DD or empty string",
  "due_date": "YYYY-MM-DD or empty string",
  "currency": "NZD",
  "amount_gross": 0,
  "gst_amount": 0,
  "amount_net": 0,
  "category_suggestion": "",
  "notes": "",
  "warnings": [],
  "confidence": "high|medium|low"
}

Rules:
- Prefer exact supplier names from the OCR text.
- If invoice number is missing, return empty string.
- Prefer the final payable amount for amount_gross. Labels like "TOTAL", "TOTAL NZD", "AMOUNT DUE", or "BALANCE DUE" are usually the gross amount.
- Do not confuse subtotal with the payable total. If both subtotal and GST exist, and a final total exists, amount_gross must be the final total, not the subtotal.
- Credits shown in parentheses such as "(120.00)" are negative adjustments, not positive amounts.
- If GST is not explicit, estimate only when the text strongly implies it; otherwise use 0 and add a warning.
- If amount_net is missing but amount_gross and gst_amount are available, set amount_net = amount_gross - gst_amount.
- Prefer NZD unless another currency is clearly shown.
- Category suggestion should be short and accounting-friendly, e.g. office_supplies, travel, software, utilities.
- warnings must be an array of short strings.
- JSON only.
"""

_TOTAL_LABEL_RE = re.compile(
    r"(?im)\b(?:total(?:\s+nzd)?|amount\s+due|balance\s+due)\b[^\d\-()]{0,20}(\(?-?\$?\d[\d,]*\.\d{2}\)?)"
)
_SUBTOTAL_LABEL_RE = re.compile(
    r"(?im)\bsubtotal\b[^\d\-()]{0,20}(\(?-?\$?\d[\d,]*\.\d{2}\)?)"
)
_GST_LABEL_RE = re.compile(
    r"(?im)\b(?:gst|tax)(?:\s+\d{1,2}%?)?\b[^\d\-()]{0,20}(\(?-?\$?\d[\d,]*\.\d{2}\)?)"
)


def _normalize_model_name(model_name: str) -> str:
    normalized = (model_name or "").strip()
    if not normalized:
        return DEFAULT_MINIMAX_MODEL

    mapping = {
        "minimax/minimax-m2.7": "MiniMax-M2.7",
        "minimax-m2.7": "MiniMax-M2.7",
        "minimax/minimax-m2.7-highspeed": "MiniMax-M2.7-highspeed",
        "minimax-m2.7-highspeed": "MiniMax-M2.7-highspeed",
    }
    return mapping.get(normalized.lower(), normalized)


def _normalize_api_key(api_key: str) -> str:
    normalized = (api_key or "").strip()
    normalized = normalized.removeprefix("Bearer ")
    normalized = normalized.removeprefix("bearer ")
    return normalized.strip()


def _resolve_chat_completions_url(api_key: str) -> str:
    if api_key.startswith("sk-cp-"):
        return MINIMAX_TOKEN_PLAN_CHAT_COMPLETIONS_URL
    return MINIMAX_CHAT_COMPLETIONS_URL


SYSTEM_PROMPT = """You are an AI assistant for a New Zealand ERP system.
Your role is to help users query ERP data, prepare invoice drafts, and prepare customer creation requests.

You must never reveal hidden reasoning, chain-of-thought, internal notes, or thinking tags.
Do not output <think>, </think>, reasoning traces, or analysis sections.
Return only the final user-facing answer.

You can ONLY:
- Search for customers, products, subscriptions, invoices
- Create invoice drafts (status='draft')
- Prepare customer creation data
- Suggest follow-up actions

You MUST NEVER:
- Send emails directly
- Mark invoices as paid or void them
- Delete any data

When creating an invoice draft, return structured JSON:
{
  "action": "create_invoice_draft",
  "customer_name": "...",
  "items": [{"description": "...", "quantity": N, "unit_price": N.NN, "tax_rate": 0.15}],
  "currency": "NZD",
  "invoice_date": "YYYY-MM-DD",
  "due_date": "YYYY-MM-DD",
  "notes": "..."
}

When preparing a customer creation request, return structured JSON:
{
  "action": "create_customer",
  "confirmed": false,
  "name": "...",
  "company_name": "...",
  "contact_name": "...",
  "email": "...",
  "phone": "...",
  "mobile": "...",
  "address": "...",
  "website": "...",
  "customer_type": "project",
  "status": "active",
  "notes": "...",
  "contacts": [
    {
      "name": "...",
      "title": "...",
      "email": "...",
      "phone": "...",
      "mobile": "...",
      "is_primary": true,
      "notes": "..."
    }
  ]
}

Customer creation rules:
- If the user asks to create a customer, first collect the customer details.
- Before the customer is created, ask for explicit confirmation and return JSON with "confirmed": false.
- Only after the user clearly confirms in a later message, return the same JSON with "confirmed": true.
- If important details are missing, ask follow-up questions instead of inventing values.

Query format:
{
  "action": "query",
  "query_type": "revenue|stats|invoice|customer|product|expense|subscription|reminder|gst",
  "filters": {...}
}

For queries, you must only return the query intent JSON. You must not provide query result data,
totals, invoice numbers, customer names, or business facts unless they were provided by the backend.
Do not include a "result" field in query JSON. The backend will execute database queries and format
the verified answer.

Conversation style: brief, professional. Always ask user to confirm before creating anything.
Style rules:
- Do not introduce yourself unless the user asks who you are.
- Prefer concise Chinese replies when the user writes in Chinese.
- For greetings, respond in 1-3 short lines and guide the next action directly.
- Prefer action-oriented wording over feature descriptions.
- Before creating any invoice draft, ask for explicit confirmation.
- Before creating any customer, ask for explicit confirmation and wait for it.

Greeting example:
Hello. Tell me what you want to do:
- Query customers, products, invoices, or subscriptions
- Create an invoice draft
- Prepare a new customer

Example: Find unpaid invoices for customer ABC
"""


def _sanitize_ai_content(content: str) -> str:
    cleaned = re.sub(r"<think>.*?</think>", "", content or "", flags=re.DOTALL | re.IGNORECASE)
    return cleaned.strip()


async def chat_with_ai(
    user_message: str,
    context: Optional[list[dict]] = None,
) -> dict:
    """Send message to MiniMax AI and return response."""
    config = get_ai_config()
    ai_model = _normalize_model_name(config["model"])
    ai_api_key = _normalize_api_key(config["api_key"])
    api_url = _resolve_chat_completions_url(ai_api_key)

    if not ai_api_key:
        return {
            "role": "assistant",
            "content": "AI API key not configured. Please set AI_API_KEY in system config.",
        }

    messages = [{"role": "system", "content": SYSTEM_PROMPT}]

    if context:
        for msg in context[-8:]:
            role = (msg.get("role") or "").strip()
            content = (msg.get("content") or "").strip()
            if role in {"system", "user", "assistant"} and content:
                messages.append({"role": role, "content": content})

    messages.append({"role": "user", "content": user_message})

    try:
        async with httpx.AsyncClient(timeout=120) as client:
            last_error = None
            for attempt in range(2):
                try:
                    resp = await client.post(
                        api_url,
                        headers={
                            "Authorization": f"Bearer {ai_api_key}",
                            "Content-Type": "application/json",
                        },
                        json={
                            "model": ai_model,
                            "messages": messages,
                            "max_tokens": 1024,
                            "temperature": 0.3,
                        },
                    )
                    break
                except httpx.HTTPError as e:
                    last_error = e
                    if attempt == 1:
                        raise
                    await asyncio.sleep(1)
            else:
                raise last_error

            if resp.status_code != 200:
                return {
                    "role": "assistant",
                    "content": f"AI API error: {resp.status_code} {resp.text[:200]}",
                }

            data = resp.json()
            choice = data.get("choices", [{}])[0]
            content = _sanitize_ai_content(choice.get("message", {}).get("content", ""))

            return {
                "role": "assistant",
                "content": content,
                "usage": data.get("usage", {}),
            }

    except httpx.HTTPError as e:
        return {
            "role": "assistant",
            "content": f"AI service unavailable: temporary network lookup or connection error ({str(e)})",
        }
    except Exception as e:
        return {
            "role": "assistant",
            "content": f"AI service unavailable: {str(e)}",
        }


def parse_ai_draft(raw_text: str) -> Optional[dict]:
    """Parse AI response text to extract structured JSON if present."""
    match = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", raw_text, re.DOTALL)
    if match:
        try:
            return json.loads(match.group(1))
        except json.JSONDecodeError:
            pass

    match = re.search(r"\{.*\}", raw_text, re.DOTALL)
    if match:
        try:
            return json.loads(match.group(0))
        except json.JSONDecodeError:
            pass

    return None


def _parse_amount_token(token: str | None) -> float | None:
    if not token:
        return None
    normalized = token.strip().replace("$", "").replace(",", "")
    negative = normalized.startswith("(") and normalized.endswith(")")
    normalized = normalized.strip("()")
    try:
        value = float(normalized)
    except ValueError:
        return None
    return -value if negative else value


def _extract_amount(pattern: re.Pattern[str], text: str) -> float | None:
    matches = pattern.findall(text)
    if not matches:
        return None
    for token in reversed(matches):
        value = _parse_amount_token(token)
        if value is not None:
            return value
    return None


def _normalize_receipt_amounts(ocr_text: str, parsed: dict) -> dict:
    total_amount = _extract_amount(_TOTAL_LABEL_RE, ocr_text)
    subtotal_amount = _extract_amount(_SUBTOTAL_LABEL_RE, ocr_text)
    gst_amount = _extract_amount(_GST_LABEL_RE, ocr_text)

    warnings = parsed.get("warnings")
    if not isinstance(warnings, list):
        warnings = []

    current_gross = _parse_amount_token(str(parsed.get("amount_gross", "")))
    current_net = _parse_amount_token(str(parsed.get("amount_net", "")))
    current_gst = _parse_amount_token(str(parsed.get("gst_amount", "")))

    if total_amount is not None and (
        current_gross is None
        or current_gross == 0
        or (subtotal_amount is not None and abs(current_gross - subtotal_amount) < 0.01 and abs(total_amount - subtotal_amount) > 0.01)
    ):
        parsed["amount_gross"] = round(total_amount, 2)
        if "Adjusted gross amount to final payable total from OCR labels." not in warnings:
            warnings.append("Adjusted gross amount to final payable total from OCR labels.")

    if gst_amount is not None and (current_gst is None or current_gst == 0):
        parsed["gst_amount"] = round(gst_amount, 2)

    if subtotal_amount is not None:
        parsed["amount_net"] = round(subtotal_amount, 2)
    elif total_amount is not None and gst_amount is not None and (current_net is None or current_net == 0):
        parsed["amount_net"] = round(total_amount - gst_amount, 2)

    parsed["warnings"] = warnings
    return parsed


async def extract_receipt_data(ocr_text: str, original_filename: str, page_count: int) -> dict:
    if not ocr_text.strip():
        return {
            "vendor_name": "",
            "invoice_number": "",
            "invoice_date": "",
            "due_date": "",
            "currency": "NZD",
            "amount_gross": 0,
            "gst_amount": 0,
            "amount_net": 0,
            "category_suggestion": "uncategorized",
            "notes": "",
            "warnings": ["No OCR text extracted from receipt."],
            "confidence": "low",
        }

    prompt = (
        f"Original filename: {original_filename}\n"
        f"Page count: {page_count}\n\n"
        "OCR text:\n"
        f"{ocr_text}"
    )

    response = await chat_with_ai(prompt, context=[{"role": "system", "content": RECEIPT_EXTRACTION_PROMPT}])
    parsed = parse_ai_draft(response.get("content", ""))
    if not isinstance(parsed, dict):
        return {
            "vendor_name": "",
            "invoice_number": "",
            "invoice_date": "",
            "due_date": "",
            "currency": "NZD",
            "amount_gross": 0,
            "gst_amount": 0,
            "amount_net": 0,
            "category_suggestion": "uncategorized",
            "notes": "",
            "warnings": ["AI extraction failed to return structured JSON."],
            "confidence": "low",
        }

    parsed.setdefault("vendor_name", "")
    parsed.setdefault("invoice_number", "")
    parsed.setdefault("invoice_date", "")
    parsed.setdefault("due_date", "")
    parsed.setdefault("currency", "NZD")
    parsed.setdefault("amount_gross", 0)
    parsed.setdefault("gst_amount", 0)
    parsed.setdefault("amount_net", 0)
    parsed.setdefault("category_suggestion", "uncategorized")
    parsed.setdefault("notes", "")
    parsed.setdefault("warnings", [])
    parsed.setdefault("confidence", "medium")
    return _normalize_receipt_amounts(ocr_text, parsed)


def validate_draft_data(draft: dict) -> tuple[bool, str]:
    """Validate draft data before creating invoice. Returns (valid, error_message)."""
    required = ["customer_name", "items"]
    for field in required:
        if field not in draft:
            return False, f"Missing required field: {field}"

    if not isinstance(draft["items"], list) or len(draft["items"]) == 0:
        return False, "Items list cannot be empty"

    for i, item in enumerate(draft["items"]):
        if "quantity" in item and float(item["quantity"]) <= 0:
            return False, f"Item {i + 1}: quantity must be positive"
        if "unit_price" in item and float(item["unit_price"]) < 0:
            return False, f"Item {i + 1}: unit_price cannot be negative"

    return True, ""


def validate_customer_data(data: dict) -> tuple[bool, str]:
    """Validate customer creation data before creating a customer."""
    if not isinstance(data, dict):
        return False, "Customer payload must be an object"

    name = str(data.get("name") or data.get("company_name") or "").strip()
    if not name:
        return False, "Missing required field: name"

    contacts = data.get("contacts", [])
    if contacts is None:
        contacts = []
    if not isinstance(contacts, list):
        return False, "Contacts must be a list"

    for index, contact in enumerate(contacts):
        if not isinstance(contact, dict):
            return False, f"Contact {index + 1} must be an object"
        if not str(contact.get("name", "")).strip():
            return False, f"Contact {index + 1}: missing required field: name"

    return True, ""
