# apps/aroflo_connector_app/agent/cheap/zone_resolvers/tasks.py
from __future__ import annotations

from typing import Any, Dict, Set, List

from ..plan import Plan, MissingParam
from ..extractors import (
    extract_task_index,
    extract_due_date_range,
    extract_page_and_pagesize,
    extract_where_tail,
    extract_join,
    extract_kv_params,
    normalize_join_keyword,
)
from ..context_bridge import get_task_ref_by_index


# Valid join-based ops present in your ops list:
# get_tasks_with_<join>
JOINABLE = {
    "assets",
    "assignedhistory",
    "customfields",
    "documentsandphotos",
    "expense",
    "labour",
    "material",
    "notes",
    "purchaseorders",
    "quote",
    "tasktotals",
    "location",
    "locationcustomfields",
    "project",
    "salesperson",
    "substatus",
}

WRITE_OPS = {
    "create_task",
    "update_task",
    "insert_task_notes",
    "insert_task_adhoc_materials",
    "mark_task_linkprocessed",
    "update_task_substatus",
    "update_task_project_stage",
}


def _choose_join_op(join: str, available_ops: Set[str]) -> str:
    j = normalize_join_keyword(join)
    if j in JOINABLE:
        candidate = f"get_tasks_with_{j}"
        if candidate in available_ops:
            return candidate
    return "list_tasks"


def build_plan(*, question: str, intent: str, available_ops: Set[str]) -> Plan:
    """
    Creates a Plan for the tasks zone using minimal parsing.
    Never invents ops: if the chosen op is not in available_ops, fallback safely.
    """
    q = question or ""

    # IMPORTANT: define missing BEFORE using it
    missing: List[MissingParam] = []

    # Base op selection
    op_code = intent if intent in available_ops else "list_tasks"
    side_effect = "write" if op_code in WRITE_OPS else "read"

    params: Dict[str, Any] = {}

    # Parse key=value (cheap structured)
    kv = extract_kv_params(q)

    # Allowed keys per operation (this is the bug area: orgid must be allowed for create_task)
    READ_KEYS = {
        "page", "pageSize", "pagesize", "where", "order", "join",
        "taskid", "TaskID",
    }

    CREATE_KEYS = {
        # REQUIRED in your tenant (confirmed by API error 511)
        "orgid",
        # required by doc/your zone
        "clientid",
        "tasktypeid",
        "taskname",
        "duedate",
        # optional
        "description",
        "contact_userid",
        "contactname",
        "contactphone",
        "custon",
        "priority",
        "status",
        "jobnumber",
        "refcode",
    }

    UPDATE_KEYS = {
        "taskid", "TaskID",
        "taskname",
        "status",
        "substatusid",
        "custon",
        # optional extras if your zone supports them
        "priority",
    }

    NOTES_KEYS = {"taskid", "TaskID", "notes"}
    MATERIALS_KEYS = {"taskid", "TaskID", "materials"}
    LINKPROCESSED_KEYS = {"taskids"}
    DUE_RANGE_KEYS = {"daterangetype", "fromdate", "todate", "where", "order", "page", "pageSize", "pagesize"}

    # Choose allowed keys based on op_code
    allowed = set(READ_KEYS)
    if op_code == "create_task":
        allowed |= CREATE_KEYS
    elif op_code == "update_task":
        allowed |= UPDATE_KEYS
    elif op_code == "insert_task_notes":
        allowed |= NOTES_KEYS
    elif op_code == "insert_task_adhoc_materials":
        allowed |= MATERIALS_KEYS
    elif op_code == "mark_task_linkprocessed":
        allowed |= LINKPROCESSED_KEYS
    elif op_code == "get_tasks_due_for_date_range":
        allowed |= DUE_RANGE_KEYS

    # Copy only allowed kv pairs into params
    for k, v in kv.items():
        if k in allowed:
            params[k] = v

    # Normalize pageSize/pagesize
    if "pagesize" in params and "pageSize" not in params:
        params["pageSize"] = params["pagesize"]

    # Join-based read selection
    join = extract_join(q) or (params.get("join") if isinstance(params.get("join"), str) else None)
    if op_code == "list_tasks" and join:
        op_code = _choose_join_op(join, available_ops)

    # where tail if provided (AroFlo pipe syntax)
    where = extract_where_tail(q)
    if where:
        params["where"] = where

    # paging hints from natural language
    page, pagesize = extract_page_and_pagesize(q)
    if page is not None:
        params["page"] = page
    if pagesize is not None:
        params["pageSize"] = pagesize

    # get_task: resolve taskid either explicit or from "task #N"
    if op_code == "get_task":
        taskid = params.get("taskid") or params.get("TaskID")
        if not taskid:
            idx = extract_task_index(q)
            if idx:
                ref = get_task_ref_by_index(idx)
                if ref and ref.get("taskid"):
                    taskid = ref["taskid"]
        if taskid:
            params["taskid"] = taskid
        else:
            missing.append(MissingParam("taskid", "Provide taskid=XXXX or refer to a recent list as 'task #2'."))

    # get_tasks_due_for_date_range: allow natural language range
    if op_code == "get_tasks_due_for_date_range":
        rng = extract_due_date_range(q)
        if rng:
            # Your zone uses fromdate/todate (per your base.py)
            params.setdefault("daterangetype", "DueDate")
            params.setdefault("fromdate", rng[0])
            params.setdefault("todate", rng[1])
        else:
            # If user didn't provide, require them
            if not params.get("fromdate") or not params.get("todate"):
                missing.append(MissingParam("fromdate/todate", "Use: due between YYYY-MM-DD and YYYY-MM-DD"))

    # create_task required fields (per your tenant + doc)
    if op_code == "create_task":
        # accept alias "summary" just in case user uses it
        if "summary" in params and "taskname" not in params:
            params["taskname"] = params["summary"]

        for req in ("orgid", "clientid", "tasktypeid", "taskname", "duedate"):
            if not params.get(req):
                missing.append(MissingParam(req, f"Provide {req}=..."))

    # Writes require confirmation
    needs_confirmation = (side_effect == "write")

    # Validate op exists; fallback if not
    if op_code not in available_ops:
        op_code = "list_tasks"
        side_effect = "read"
        needs_confirmation = False

    summary = f"tasks.{op_code} ({side_effect})"
    if join and side_effect == "read":
        summary += f" join={join}"

    return Plan(
        zone_code="tasks",
        op_code=op_code,
        params=params,
        side_effect=side_effect,  # type: ignore[arg-type]
        needs_confirmation=needs_confirmation,
        summary=summary,
        missing=missing,
    )
