#!/usr/bin/env python3
"""
Odoo MCP Server.
Provides basic CRUD operations, introspection, BI tools, and workflow navigation.
"""

import csv
import http.cookiejar
import io
import json
import os
import time
import urllib.request
import xml.etree.ElementTree as ET
from typing import Any, Dict, List, Optional

from mcp.server.fastmcp import FastMCP


# --- Configuration ---

ODOO_URL = os.environ.get("ODOO_URL", "")
ODOO_DB = os.environ.get("ODOO_DB", "")
ODOO_USERNAME = os.environ.get("ODOO_USERNAME", "")
ODOO_PASSWORD = os.environ.get("ODOO_PASSWORD", "")
AUTH_CACHE_TTL = int(os.environ.get("MCP_AUTH_CACHE_TTL", "300"))

mcp = FastMCP("odoo-mcp-server")


# --- Infrastructure: Auth Cache & JSON-RPC Execution ---

_auth_cache: Dict[str, Any] = {"uid": None, "timestamp": 0.0}
_cookie_jar = http.cookiejar.CookieJar()
_opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(_cookie_jar))

def _jsonrpc(endpoint: str, params: Dict[str, Any]) -> Any:
    """Make a JSON-RPC call to Odoo, returns the 'result' value."""
    payload = json.dumps({"jsonrpc": "2.0", "method": "call", "params": params}).encode()
    req = urllib.request.Request(
        f"{ODOO_URL}{endpoint}",
        data=payload,
        headers={"Content-Type": "application/json"},
    )
    response = json.loads(_opener.open(req).read())
    if "error" in response:
        raise RuntimeError(response["error"].get("data", {}).get("message", str(response["error"])))
    return response.get("result")

def _get_uid() -> Optional[int]:
    """Authenticate via JSON-RPC and cache the session. Re-authenticates if TTL expired."""
    now = time.time()
    if _auth_cache["uid"] and (now - _auth_cache["timestamp"]) < AUTH_CACHE_TTL:
        return _auth_cache["uid"]
    result = _jsonrpc("/web/session/authenticate", {
        "db": ODOO_DB,
        "login": ODOO_USERNAME,
        "password": ODOO_PASSWORD,
    })
    uid = result.get("uid") if result else None
    if uid:
        _auth_cache["uid"] = uid
        _auth_cache["timestamp"] = now
    return uid

def safe_execute_kw(
    model: str,
    method: str,
    args: List[Any],
    kwargs: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    """Execute an Odoo RPC call with cached session, returning {success, data/error}."""
    try:
        if not all([ODOO_URL, ODOO_DB, ODOO_USERNAME, ODOO_PASSWORD]):
            return {
                "success": False,
                "error": "Missing credentials. Set ODOO_URL, ODOO_DB, ODOO_USERNAME, ODOO_PASSWORD.",
            }
        uid = _get_uid()
        if not uid:
            return {"success": False, "error": "Authentication failed. Check credentials."}
        result = _jsonrpc("/web/dataset/call_kw", {
            "model": model,
            "method": method,
            "args": args,
            "kwargs": kwargs or {},
        })
        return {"success": True, "data": result}
    except RuntimeError as e:
        _auth_cache["uid"] = None
        return {"success": False, "error": f"Odoo error: {e}"}
    except Exception as e:
        _auth_cache["uid"] = None
        return {"success": False, "error": str(e)}

# --- Core CRUD Tools ---

@mcp.tool()
def search_records(
    model: str, domain: List[Any], limit: int = 10, offset: int = 0
) -> Dict[str, Any]:
    """
    Search for record IDs matching a domain filter.
    Args:
        model: Odoo model (e.g. 'res.partner')
        domain: Domain list (e.g. [['is_company','=',True]])
        limit: Max records (default 10)
        offset: Pagination offset
    Returns: list of integer IDs
    """
    return safe_execute_kw(model, "search", [domain], {"limit": limit, "offset": offset})

@mcp.tool()
def read_records(
    model: str, ids: List[int], fields: Optional[List[str]] = None
) -> Dict[str, Any]:
    """
    Read fields from records by their IDs.
    Args:
        model: Odoo model (e.g. 'res.partner')
        ids: Record IDs to read
        fields: Fields to return (all if omitted)
    """
    kw = {"fields": fields} if fields else {}
    return safe_execute_kw(model, "read", [ids], kw)

@mcp.tool()
def search_read_records(
    model: str,
    domain: List[Any],
    fields: Optional[List[str]] = None,
    limit: int = 10,
    offset: int = 0,
    order: str = "",
) -> Dict[str, Any]:
    """
    Search and read in one call (optimised).
    Args:
        model: Odoo model
        domain: Domain filter
        fields: Fields to read (all if omitted)
        limit: Max records (default 10)
        offset: Pagination offset
        order: Sort (e.g. 'name asc, date desc')
    """
    kw: Dict[str, Any] = {"limit": limit, "offset": offset}
    if fields:
        kw["fields"] = fields
    if order:
        kw["order"] = order
    return safe_execute_kw(model, "search_read", [domain], kw)

@mcp.tool()
def create_record(model: str, values: Dict[str, Any]) -> Dict[str, Any]:
    """
    Create a new record.
    Args:
        model: Odoo model (e.g. 'res.partner')
        values: Field values (e.g. {'name':'John','email':'j@x.com'})
    Returns: ID of the new record
    """
    return safe_execute_kw(model, "create", [values])

@mcp.tool()
def update_record(model: str, ids: List[int], values: Dict[str, Any]) -> Dict[str, Any]:
    """
    Update existing records.
    Args:
        model: Odoo model
        ids: Record IDs to update
        values: New field values (e.g. {'phone':'+56912345678'})
    Returns: True on success
    """
    return safe_execute_kw(model, "write", [ids, values])

@mcp.tool()
def delete_record(model: str, ids: List[int]) -> Dict[str, Any]:
    """
    Delete records. USE WITH CAUTION.
    Args:
        model: Odoo model
        ids: Record IDs to delete
    Returns: True on success
    """
    return safe_execute_kw(model, "unlink", [ids])

@mcp.tool()
def count_records(model: str, domain: List[Any]) -> Dict[str, Any]:
    """
    Count matching records without loading them (very fast).
    Args:
        model: Odoo model
        domain: Domain filter (e.g. [['state','=','sale']])
    Returns: integer count
    """
    return safe_execute_kw(model, "search_count", [domain])


# --- Schema & Introspection Tools ---

@mcp.tool()
def get_model_fields(
    model: str, attributes: Optional[List[str]] = None
) -> Dict[str, Any]:
    """
    Discover the schema of an Odoo model.
    Args:
        model: Odoo model (e.g. 'res.partner')
        attributes: Which metadata to return per field
                    (default: type, string, help, relation, required, readonly, selection)
    """
    attrs = attributes or [
        "type", "string", "help", "relation", "required", "readonly", "selection",
    ]
    return safe_execute_kw(model, "fields_get", [], {"attributes": attrs})

@mcp.tool()
def get_view_architecture(
    model: str, view_type: str = "form", view_id: Optional[int] = None
) -> Dict[str, Any]:
    """
    Get the FINAL merged XML of an Odoo view (base + all inheritances).
    Args:
        model: Odoo model (e.g. 'res.partner')
        view_type: 'form', 'tree', 'kanban', 'search', etc.
        view_id: Specific view ID (optional)
    Returns: complete XML string as the user sees it on screen
    """
    vid = view_id or False
    res = safe_execute_kw(model, "get_views", [[[vid, view_type]]], {})
    if res["success"]:
        views = res["data"].get("views", {})
        res["data"] = views.get(view_type, {}).get("arch", "No XML architecture found.")
    return res

@mcp.tool()
def execute_method(
    model: str,
    method: str,
    args: List[Any],
    kwargs: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    """
    Call ANY public method on ANY Odoo model. Infinitely flexible.
    Args:
        model: Odoo model (e.g. 'sale.order')
        method: Method name (e.g. 'action_confirm')
        args: Positional args (often [record_ids])
        kwargs: Keyword args
    Examples:
        Confirm sale order:  model='sale.order', method='action_confirm', args=[[42]]
        Validate picking:    model='stock.picking', method='button_validate', args=[[7]]
    """
    return safe_execute_kw(model, method, args, kwargs)

# --- Navigation Tools ---

@mcp.tool()
def get_workflow_states(model: str) -> Dict[str, Any]:
    """
    Discover workflow states and transitions of an Odoo model.
    Returns the selection values of the 'state' field, stage_id records,
    and the action buttons that trigger transitions.
    Args:
        model: Odoo model (e.g. 'sale.order')
    """
    data: Dict[str, Any] = {
        "model": model,
        "state_field": None,
        "states": [],
        "stage_field": None,
        "stages": [],
        "action_buttons": [],
    }
    # --- Detect state / stage fields ---
    fields_res = safe_execute_kw(
        model, "fields_get", [], {"attributes": ["type", "string", "selection", "relation"]}
    )
    if not fields_res["success"]:
        return fields_res
    fields = fields_res["data"]

    if "state" in fields and fields["state"].get("type") == "selection":
        data["state_field"] = "state"
        data["states"] = fields["state"].get("selection", [])

    if "stage_id" in fields and fields["stage_id"].get("type") == "many2one":
        data["stage_field"] = "stage_id"
        stage_model = fields["stage_id"].get("relation", "")
        if stage_model:
            stages_res = safe_execute_kw(
                stage_model, "search_read", [[]],
                {"fields": ["name", "sequence"], "order": "sequence"},
            )
            if stages_res["success"]:
                data["stages"] = stages_res["data"]

    # --- Extract action buttons from form view ---
    view_res = safe_execute_kw(model, "get_views", [[[False, "form"]]], {})
    if view_res["success"]:
        arch = view_res["data"].get("views", {}).get("form", {}).get("arch", "")
        try:
            root = ET.fromstring(arch)
            for btn in root.iter("button"):
                name = btn.get("name", "")
                if not name:
                    continue
                data["action_buttons"].append({
                    "name": name,
                    "label": btn.get("string", ""),
                    "type": btn.get("type", "object"),
                    "states": btn.get("states", ""),
                    "groups": btn.get("groups", ""),
                    "invisible": btn.get("invisible", ""),
                })
        except ET.ParseError:
            data["xml_parse_error"] = "Could not parse form view XML."

    return {"success": True, "data": data}


@mcp.tool()
def get_action_buttons(model: str, view_type: str = "form") -> Dict[str, Any]:
    """
    Extract all action buttons from an Odoo view.
    Shows what actions are available, when they're visible (states/invisible),
    and who can use them (groups).
    Args:
        model: Odoo model (e.g. 'sale.order')
        view_type: View to inspect (default 'form')
    """
    view_res = safe_execute_kw(model, "get_views", [[[False, view_type]]], {})
    if not view_res["success"]:
        return view_res
    arch = view_res["data"].get("views", {}).get(view_type, {}).get("arch", "")
    buttons = []
    try:
        root = ET.fromstring(arch)
        for btn in root.iter("button"):
            name = btn.get("name", "")
            if not name:
                continue
            buttons.append({
                "name": name,
                "label": btn.get("string", ""),
                "type": btn.get("type", "object"),
                "states": btn.get("states", "").split(",") if btn.get("states") else [],
                "groups": btn.get("groups", ""),
                "confirm": btn.get("confirm", ""),
                "invisible": btn.get("invisible", ""),
                "class": btn.get("class", ""),
                "icon": btn.get("icon", ""),
            })
    except ET.ParseError:
        return {"success": False, "error": "Could not parse view XML."}
    return {"success": True, "data": {"model": model, "view_type": view_type, "buttons": buttons}}


@mcp.tool()
def get_related_records(
    model: str, record_id: int, max_relations: int = 10
) -> Dict[str, Any]:
    """
    Discover all records related to a given record.
    Automatically finds Many2one, One2many and Many2many relations and
    returns a summary of each.
    Args:
        model: Odoo model (e.g. 'sale.order')
        record_id: ID of the record to inspect
        max_relations: Max related records to show per field (default 10)
    """
    # 1. Get relation field definitions
    fields_res = safe_execute_kw(
        model, "fields_get", [], {"attributes": ["type", "string", "relation"]}
    )
    if not fields_res["success"]:
        return fields_res
    rel_fields = {
        fn: fd for fn, fd in fields_res["data"].items()
        if fd.get("type") in ("many2one", "one2many", "many2many") and fd.get("relation")
    }
    if not rel_fields:
        return {"success": True, "data": {"message": "No relation fields on this model."}}

    # 2. Read the record's relation values
    rec_res = safe_execute_kw(model, "read", [[record_id]], {"fields": list(rel_fields.keys())})
    if not rec_res["success"]:
        return rec_res
    if not rec_res["data"]:
        return {"success": False, "error": f"Record {record_id} not found in {model}"}
    record = rec_res["data"][0]

    # 3. Build summary per relation
    result: Dict[str, Any] = {}
    for fname, fdef in rel_fields.items():
        value = record.get(fname)
        if not value:
            continue
        ftype = fdef["type"]
        target = fdef["relation"]

        if ftype == "many2one" and isinstance(value, (list, tuple)) and len(value) == 2:
            result[fname] = {
                "type": "many2one",
                "label": fdef.get("string", fname),
                "target_model": target,
                "value": {"id": value[0], "name": value[1]},
            }
        elif ftype in ("one2many", "many2many") and isinstance(value, list):
            ids_slice = value[:max_relations]
            names_res = safe_execute_kw(target, "name_get", [ids_slice])
            recs = (
                [{"id": r[0], "name": r[1]} for r in names_res["data"]]
                if names_res["success"]
                else [{"id": rid} for rid in ids_slice]
            )
            result[fname] = {
                "type": ftype,
                "label": fdef.get("string", fname),
                "target_model": target,
                "total_count": len(value),
                "records": recs,
                "truncated": len(value) > max_relations,
            }
    return {"success": True, "data": result}


@mcp.tool()
def explain_domain(domain: List[Any]) -> Dict[str, Any]:
    """
    Translate an Odoo domain filter into human-readable language.
    Args:
        domain: Odoo domain (e.g. [['state','=','sale'],['amount_total','>',1000]])
    Returns: natural-language explanation of the filter
    """
    OPS = {
        "=": "equals", "!=": "does not equal",
        ">": ">", ">=": ">=", "<": "<", "<=": "<=",
        "like": "contains (case-sensitive)", "ilike": "contains",
        "not like": "does not contain (case-sensitive)", "not ilike": "does not contain",
        "in": "is one of", "not in": "is not one of",
        "=like": "matches pattern", "=ilike": "matches pattern (ci)",
        "child_of": "is child of", "parent_of": "is parent of",
    }
    if not isinstance(domain, list):
        return {"success": False, "error": "Domain must be a list."}
    parts: List[str] = []
    next_joiner = "AND"
    next_negate = False
    condition_count = 0
    for item in domain:
        if item == "&":
            next_joiner = "AND"
        elif item == "|":
            next_joiner = "OR"
        elif item == "!":
            next_negate = True
        elif isinstance(item, (list, tuple)) and len(item) == 3:
            field, op, val = item
            if not isinstance(op, str):
                continue  # skip malformed conditions
            op_txt = OPS.get(op, op)
            if isinstance(val, str):
                val_txt = f"'{val}'"
            elif val is False or val is None:
                val_txt = "empty/not set"
            else:
                val_txt = str(val)
            condition = f"{field} {op_txt} {val_txt}"
            if next_negate:
                condition = f"NOT ({condition})"
                next_negate = False
            if parts:
                prev = parts.pop()
                condition = f"{prev} {next_joiner} {condition}"
                next_joiner = "AND"
            parts.append(condition)
            condition_count += 1
        # skip unrecognised items silently

    text = " AND ".join(parts) if parts else "Empty domain (matches all records)"
    return {
        "success": True,
        "data": {"original_domain": domain, "explanation": text, "filter_count": condition_count},
    }


@mcp.tool()
def get_security_rules(model: str) -> Dict[str, Any]:
    """
    Show access rights (ACL) and record rules for an Odoo model.
    Reveals who can read/write/create/delete and what record-level restrictions apply.
    Args:
        model: Odoo model (e.g. 'sale.order')
    """
    data: Dict[str, Any] = {"model": model, "access_rights": [], "record_rules": []}
    acl = safe_execute_kw(
        "ir.model.access", "search_read",
        [[["model_id.model", "=", model]]],
        {"fields": ["name", "group_id", "perm_read", "perm_write", "perm_create", "perm_unlink"]},
    )
    if acl["success"]:
        data["access_rights"] = acl["data"]
    rules = safe_execute_kw(
        "ir.rule", "search_read",
        [[["model_id.model", "=", model]]],
        {"fields": [
            "name", "domain_force", "groups", "perm_read", "perm_write",
            "perm_create", "perm_unlink", "active",
        ]},
    )
    if rules["success"]:
        data["record_rules"] = rules["data"]
    return {"success": True, "data": data}


# --- BI Tools ---


@mcp.tool()
def read_group(
    model: str,
    domain: List[Any],
    fields: List[str],
    groupby: List[str],
    limit: int = 80,
    orderby: str = "",
    lazy: bool = True,
) -> Dict[str, Any]:
    """
    Aggregate and group records — Odoo's equivalent of SQL GROUP BY.
    Returns sums, counts and averages per group.
    Args:
        model: Odoo model (e.g. 'sale.order')
        domain: Filter (e.g. [['state','=','sale']])
        fields: Fields to aggregate (e.g. ['amount_total','partner_id'])
        groupby: Group-by fields (e.g. ['partner_id','state'])
                 Use ':month'/':year' for dates (e.g. 'date_order:month')
        limit: Max groups (default 80)
        orderby: Sort (e.g. 'amount_total desc')
        lazy: Group only by first field when True (default True)
    Examples:
        Sales by customer:  fields=['amount_total'], groupby=['partner_id']
        Revenue by month:   fields=['amount_total'], groupby=['date_order:month']
        Stock by warehouse:  fields=['quantity'], groupby=['location_id']
    """
    kw: Dict[str, Any] = {"lazy": lazy}
    if limit is not None:
        kw["limit"] = limit
    if orderby:
        kw["orderby"] = orderby
    return safe_execute_kw(model, "read_group", [domain, fields, groupby], kw)


@mcp.tool()
def get_available_reports(model: str) -> Dict[str, Any]:
    """
    List all available reports (PDF / HTML) for an Odoo model.
    Args:
        model: Odoo model (e.g. 'sale.order')
    Returns: list of reports with name, type and technical report_name
    """
    return safe_execute_kw(
        "ir.actions.report", "search_read",
        [[["model", "=", model]]],
        {"fields": ["name", "report_name", "report_type", "print_report_name"]},
    )


@mcp.tool()
def export_to_csv(
    model: str,
    domain: List[Any],
    fields: Optional[List[str]] = None,
    limit: int = 1000,
    order: str = "",
) -> Dict[str, Any]:
    """
    Export records directly to a CSV formatted string.
    Args:
        model: Odoo model.
        domain: Domain filter (e.g. [['state','=','posted']]).
        fields: Fields to export (all if omitted).
        limit: Max records to export (default 1000).
        order: Sort order.
    Returns: A dictionary containing the raw 'csv_content' and 'record_count'.
    """
    kw: Dict[str, Any] = {"limit": limit}
    if fields:
        kw["fields"] = fields
    if order:
        kw["order"] = order

    res = safe_execute_kw(model, "search_read", [domain], kw)
    if not res.get("success"):
        return res

    records = res.get("data", [])
    if not records:
        return {"success": True, "data": {"csv_content": "", "record_count": 0}}

    output = io.StringIO()
    export_fields = fields if fields else list(records[0].keys())
    writer = csv.DictWriter(output, fieldnames=export_fields, extrasaction='ignore')
    writer.writeheader()

    for row in records:
        clean_row = {}
        for f in export_fields:
            val = row.get(f)
            # Flatten many2one fields (which are [id, "name"]) to just "name"
            if isinstance(val, (list, tuple)) and len(val) == 2 and isinstance(val[0], int):
                clean_row[f] = val[1]
            # Flatten one2many/many2many lists to comma-separated strings
            elif isinstance(val, (list, tuple)):
                clean_row[f] = ",".join(map(str, val))
            else:
                clean_row[f] = val
        writer.writerow(clean_row)

    return {
        "success": True,
        "data": {
            "csv_content": output.getvalue(),
            "record_count": len(records)
        }
    }

# --- MCP Resources ---

@mcp.resource("odoo://models")
def list_models() -> str:
    """List every model installed in this Odoo instance."""
    res = safe_execute_kw(
        "ir.model", "search_read", [[]], {"fields": ["model", "name"], "order": "model"}
    )
    if not res["success"]:
        return "Error: " + res["error"]
    return "\n".join(f"- {r['model']} ({r['name']})" for r in res["data"])

@mcp.resource("odoo://fields/{model}")
def fields_resource(model: str) -> str:
    """Get field definitions for an Odoo model (formatted text)."""
    res = safe_execute_kw(
        model, "fields_get", [], {"attributes": ["type", "string", "relation", "required"]}
    )
    if not res["success"]:
        return "Error: " + res["error"]
    lines = []
    for fn, fd in sorted(res["data"].items()):
        rel = f" -> {fd['relation']}" if fd.get("relation") else ""
        req = " [REQUIRED]" if fd.get("required") else ""
        lines.append(f"- {fn} ({fd.get('type','?')}{rel}): {fd.get('string','')}{req}")
    return "\n".join(lines)

@mcp.resource("odoo://record/{model}/{record_id}")
def record_resource(model: str, record_id: str) -> str:
    """Read a single Odoo record by model and ID."""
    try:
        rid = int(record_id)
    except ValueError:
        return f"Error: '{record_id}' is not a valid integer ID"
    res = safe_execute_kw(model, "read", [[rid]])
    if not res["success"]:
        return "Error: " + res["error"]
    if not res["data"]:
        return f"Record {rid} not found in {model}"
    rec = res["data"][0]
    lines = [f"{model} (ID: {rid})"]
    for k, v in rec.items():
        lines.append(f"  {k}: {v}")
    return "\n".join(lines)


@mcp.resource("odoo://search/{model}/{domain_str}")
def search_resource(model: str, domain_str: str) -> str:
    """
    Search records via URI.  domain_str must be JSON-encoded.
    Example: odoo://search/res.partner/[["is_company","=",true]]
    """
    try:
        domain = json.loads(domain_str)
    except json.JSONDecodeError:
        return f"Error: could not parse domain '{domain_str}' — must be valid JSON."
    res = safe_execute_kw(model, "search_read", [domain], {"limit": 20})
    if not res["success"]:
        return "Error: " + res["error"]
    lines = [f"Search results for {model}:"]
    for r in res["data"]:
        name = r.get("name", r.get("display_name", f"ID {r.get('id','?')}"))
        lines.append(f"  - [{r.get('id','?')}] {name}")
    return "\n".join(lines)

# --- MCP Prompts ---
@mcp.prompt()
def analyze_sales(period: str = "this_month") -> str:
    """Guided workflow to analyse sales performance and KPIs."""
    return (
        f"Analyse sales performance for the period: {period}.\n\n"
        "Steps:\n"
        "1. read_group on 'sale.order' with [['state','in',['sale','done']]]:\n"
        "   - amount_total grouped by date_order:month  (revenue trend)\n"
        "   - amount_total grouped by partner_id         (top customers)\n"
        "   - amount_total grouped by user_id            (sales by rep)\n"
        "2. count_records for total vs confirmed orders.\n"
        "3. Calculate: average order value, conversion rate, top-5 customers.\n"
        "4. Present a clear summary with actionable insights.\n"
    )

@mcp.prompt()
def diagnose_inventory(product: str = "") -> str:
    """Guided workflow to diagnose inventory issues."""
    f = f" for product '{product}'" if product else ""
    return (
        f"Diagnose inventory status{f}.\n\n"
        "Steps:\n"
        "1. search_read_records 'product.product' where type='product' — "
        "   fields: name, qty_available, virtual_available.\n"
        "2. read_group 'stock.move' by state to find pending/stuck moves.\n"
        "3. read_group 'stock.quant' grouped by location_id for distribution.\n"
        "4. Identify: low-stock products, stuck transfers, warehouse utilisation.\n"
        "5. Provide optimisation recommendations.\n"
    )

@mcp.prompt()
def audit_permissions(user_login: str = "") -> str:
    """Guided workflow to audit user permissions and access rights."""
    f = f" for user '{user_login}'" if user_login else ""
    return (
        f"Audit user permissions{f}.\n\n"
        "Steps:\n"
        "1. search_read_records 'res.users' to locate the user(s).\n"
        "2. Read groups_id to list security groups.\n"
        "3. get_security_rules on sale.order, purchase.order, account.move, stock.picking.\n"
        "4. Cross-reference groups with ACLs.\n"
        "5. Flag over-privileged users, missing rights, conflicting rules.\n"
        "6. Provide a summary with recommendations.\n"
    )

@mcp.prompt()
def financial_overview(date_from: str = "", date_to: str = "") -> str:
    """Guided workflow for a financial overview."""
    p = f" from {date_from} to {date_to}" if date_from else " for the current period"
    return (
        f"Generate a financial overview{p}.\n\n"
        "Steps:\n"
        "1. read_group 'account.move':\n"
        "   - Revenue: move_type='out_invoice', state='posted' by month.\n"
        "   - Expenses: move_type='in_invoice', state='posted' by month.\n"
        "2. Unpaid invoices: payment_state != 'paid'.\n"
        "3. KPIs: total revenue vs expenses, AR aging (30/60/90d), top debtors.\n"
        "4. Present a clear financial summary.\n"
    )

# --- Entry Point ---

if __name__ == "__main__":
    transport = os.environ.get("MCP_TRANSPORT", "stdio")
    if transport == "stdio":
        mcp.run()
    else:
        mcp.run(transport=transport)
