# /apps/aroflo_connector_app/zones/timesheets/base.py
from __future__ import annotations

from datetime import date
from typing import Any, Dict, List, Tuple, Optional

from ..base import ZoneOperation, ParamSpec
from ._join_utils import request, raw_wrap, coerce_page_size, coerce_order
from datetime import date


# -------------------------
# Operation codes (READ)
# -------------------------
OP_LIST = "list_timesheets"
OP_GET = "get_timesheet"

OP_BY_USER = "get_timesheets_by_user"
OP_BY_TASK = "get_timesheets_by_task"
OP_BY_TYPE = "get_timesheets_by_type"
OP_AFTER_WORKDATE = "get_timesheets_after_workdate"
OP_BY_USER_RANGE = "get_timesheets_by_user_range"


def _common_list_paramspec() -> List[ParamSpec]:
    return [
        ParamSpec("where", "string", False, "Cláusula WHERE estilo AroFlo."),
        ParamSpec("order", "string", False, "Orden: campo|asc o campo|desc (AroFlo 'order')."),
        ParamSpec("page", "integer", False, "Número de página (1..N)."),
        ParamSpec("pageSize", "integer", False, "Cantidad de registros por página (AroFlo pageSize)."),
        ParamSpec("raw", "boolean", False, "Si true, devuelve respuesta cruda + meta debug."),
    ]


def get_operations() -> List[ZoneOperation]:
    return [
        ZoneOperation(
            code=OP_LIST,
            label="List Timesheets",
            description="Lista timesheets (zone=timesheets) con WHERE/ORDER y paginación.",
            http_method="GET",
            side_effect="read",
            idempotent=True,
            default_params={
                "where": ["and|workdate|>|2001-01-01"],
                "order": None,
                "page": 1,
                "pageSize": None,
                "raw": False,
            },
            params=_common_list_paramspec(),
            category="timesheets",
            use_cases=["Listar timesheets", "Filtrar timesheets con WHERE", "Ordenar timesheets con ORDER"],
            risk_level="low",
        ),
        ZoneOperation(
            code=OP_GET,
            label="Get Timesheet",
            description="Obtiene un registro específico por timesheetid (where=and|timesheetid|=|...).",
            http_method="GET",
            side_effect="read",
            idempotent=True,
            default_params={"raw": False},
            params=[
                ParamSpec("timesheetid", "string", True, "TimesheetID (AroFlo)."),
                ParamSpec("raw", "boolean", False, "Si true, devuelve respuesta cruda + meta debug."),
            ],
            category="timesheets",
            use_cases=["Consultar detalle de un timesheet por timesheetid"],
            risk_level="low",
        ),
        ZoneOperation(
            code=OP_BY_USER,
            label="Timesheets by User",
            description="Lista timesheets filtrados por userid (AroFlo).",
            http_method="GET",
            side_effect="read",
            idempotent=True,
            default_params={
                "userid": None,
                "where": None,
                "order": None,
                "page": 1,
                "pageSize": None,
                "raw": False,
            },
            params=[
                ParamSpec("userid", "string", True, "UserID (AroFlo)."),
                *_common_list_paramspec(),
            ],
            category="timesheets",
            use_cases=["Ver timesheets de un usuario", "Auditar horas por empleado"],
            risk_level="low",
        ),
        ZoneOperation(
            code=OP_BY_USER_RANGE,
            label="Timesheets by User (Date Range)",
            description=(
                "Lista timesheets de un usuario filtrando por rango de workdate (from_date..to_date) "
                "con paginación y corte local (robusto ante limitaciones del WHERE de AroFlo)."
            ),
            http_method="GET",
            side_effect="read",
            idempotent=True,
            default_params={
                "userid": None,
                "from_date": None,
                "to_date": None,
                "order": None,
                "pageSize": None,
                "raw": False,
            },
            params=[
                ParamSpec("userid", "string", True, "UserID (AroFlo)."),
                ParamSpec("from_date", "string", True, "Fecha inicio (YYYY-MM-DD)."),
                ParamSpec("to_date", "string", True, "Fecha fin (YYYY-MM-DD)."),
                ParamSpec("order", "string", False, "Orden. Default: workdate|asc."),
                ParamSpec("pageSize", "integer", False, "Cantidad de registros por página."),
                ParamSpec("raw", "boolean", False, "Si true, devuelve respuesta cruda + meta debug."),
            ],
            category="timesheets",
            use_cases=["Reporte por rango de fechas", "Payroll/leave auditing", "Sincronización incremental por ventana"],
            risk_level="low",
        ),
        ZoneOperation(
            code=OP_BY_TASK,
            label="Timesheets by Task",
            description="Lista timesheets filtrados por taskid (AroFlo).",
            http_method="GET",
            side_effect="read",
            idempotent=True,
            default_params={
                "taskid": None,
                "where": None,
                "order": None,
                "page": 1,
                "pageSize": None,
                "raw": False,
            },
            params=[
                ParamSpec("taskid", "string", True, "TaskID (AroFlo)."),
                *_common_list_paramspec(),
            ],
            category="timesheets",
            use_cases=["Ver horas asociadas a una task", "Auditar costo/tiempo por trabajo"],
            risk_level="low",
        ),
        ZoneOperation(
            code=OP_BY_TYPE,
            label="Timesheets by Type",
            description="Lista timesheets filtrados por type (según AroFlo).",
            http_method="GET",
            side_effect="read",
            idempotent=True,
            default_params={
                "type": None,
                "where": None,
                "order": None,
                "page": 1,
                "pageSize": None,
                "raw": False,
            },
            params=[
                ParamSpec("type", "string", True, "Tipo de timesheet (ej: Productive/Non-Productive u otro enum del tenant)."),
                *_common_list_paramspec(),
            ],
            category="timesheets",
            use_cases=["Filtrar por tipo", "Reportes por categoría de hora"],
            risk_level="low",
        ),
        ZoneOperation(
            code=OP_AFTER_WORKDATE,
            label="Timesheets after WorkDate",
            description="Lista timesheets posteriores a una workdate (YYYY-MM-DD) o equivalente AroFlo.",
            http_method="GET",
            side_effect="read",
            idempotent=True,
            default_params={
                "workdate": None,
                "where": None,
                "order": None,
                "page": 1,
                "pageSize": None,
                "raw": False,
            },
            params=[
                ParamSpec("workdate", "string", True, "Fecha (YYYY-MM-DD) para filtrar posteriores."),
                *_common_list_paramspec(),
            ],
            category="timesheets",
            use_cases=["Traer registros recientes", "Sincronización incremental por fecha"],
            risk_level="low",
        ),
    ]


def supports(operation_code: str) -> bool:
    return operation_code in {
        OP_LIST,
        OP_GET,
        OP_BY_USER,
        OP_BY_TASK,
        OP_BY_TYPE,
        OP_AFTER_WORKDATE,
        OP_BY_USER_RANGE,
    }


def execute(operation_code: str, client: Any, params: Dict[str, Any]) -> Any:
    raw = bool(params.get("raw", False))
    pageSize = coerce_page_size(params)
    order = coerce_order(params)

    def _coerce_wheres(v: Any, *, default: List[str]) -> List[str]:
        """
        Normaliza 'where' para que siempre sea List[str].
        - None -> default
        - list/tuple -> lista limpia
        - string -> [string]
        """
        if v is None:
            return list(default)
        if isinstance(v, (list, tuple)):
            return [str(x) for x in v if str(x).strip()]
        s = str(v).strip()
        return [s] if s else list(default)

    def _do_list(*, wheres: List[str], page: int) -> Any:
        params_list: List[Tuple[str, str]] = [("zone", "timesheets")]

        for w in wheres:
            params_list.append(("where", w))

        if order:
            params_list.append(("order", order))
        if pageSize:
            params_list.append(("pageSize", str(pageSize)))

        params_list.append(("page", str(page)))

        resp = request(client, "GET", params_list)
        return raw_wrap(resp, params_list) if raw else resp

    def _parse_workdate(s: str) -> Optional[date]:
        # AroFlo suele devolver YYYY/MM/DD (pero a veces llega YYYY-MM-DD)
        if not s:
            return None
        s = str(s).strip()
        if not s or s == " ":
            return None
        sep = "/" if "/" in s else "-"
        parts = s.split(sep)
        if len(parts) != 3:
            return None
        y, m, d = (int(parts[0]), int(parts[1]), int(parts[2]))
        return date(y, m, d)

    def _parse_ymd(s: str) -> date:
        # input esperado YYYY-MM-DD
        y, m, d = s.split("-")
        return date(int(y), int(m), int(d))

    # -------------------------
    # OP_LIST
    # -------------------------
    if operation_code == OP_LIST:
        wheres = _coerce_wheres(params.get("where"), default=["and|workdate|>|2001-01-01"])
        page = int(params.get("page", 1))
        if page < 1:
            raise ValueError("page debe ser >= 1.")
        return _do_list(wheres=wheres, page=page)

    # -------------------------
    # OP_GET
    # -------------------------
    if operation_code == OP_GET:
        timesheetid = params["timesheetid"]
        where = f"and|timesheetid|=|{timesheetid}"
        params_list: List[Tuple[str, str]] = [("zone", "timesheets"), ("where", where), ("page", "1")]
        resp = request(client, "GET", params_list)
        return raw_wrap(resp, params_list) if raw else resp

    # -------------------------
    # OP_BY_USER
    # -------------------------
    if operation_code == OP_BY_USER:
        userid = str(params["userid"])
        wheres = _coerce_wheres(params.get("where"), default=["and|workdate|>|2001-01-01"])
        # Forzar filtro por usuario (AroFlo no respeta ?userid= en timesheets)
        wheres = [f"and|userid|=|{userid}", *wheres]

        page = int(params.get("page", 1))
        if page < 1:
            raise ValueError("page debe ser >= 1.")
        return _do_list(wheres=wheres, page=page)

    # -------------------------
    # OP_BY_TASK
    # -------------------------
    if operation_code == OP_BY_TASK:
        taskid = str(params["taskid"])
        wheres = _coerce_wheres(params.get("where"), default=["and|workdate|>|2001-01-01"])
        wheres = [f"and|taskid|=|{taskid}", *wheres]

        page = int(params.get("page", 1))
        if page < 1:
            raise ValueError("page debe ser >= 1.")
        return _do_list(wheres=wheres, page=page)

    # -------------------------
    # OP_BY_TYPE
    # -------------------------
    if operation_code == OP_BY_TYPE:
        t = str(params["type"])
        wheres = _coerce_wheres(params.get("where"), default=["and|workdate|>|2001-01-01"])
        wheres = [f"and|type|=|{t}", *wheres]

        page = int(params.get("page", 1))
        if page < 1:
            raise ValueError("page debe ser >= 1.")
        return _do_list(wheres=wheres, page=page)

    # -------------------------
    # OP_AFTER_WORKDATE
    # -------------------------
    if operation_code == OP_AFTER_WORKDATE:
        workdate = str(params["workdate"])
        base_wheres = _coerce_wheres(params.get("where"), default=[])
        wheres = [*base_wheres, f"and|workdate|>|{workdate}"]

        page = int(params.get("page", 1))
        if page < 1:
            raise ValueError("page debe ser >= 1.")
        return _do_list(wheres=wheres, page=page)

    # -------------------------
    # OP_BY_USER_RANGE (robusto)
    # -------------------------
    if operation_code == OP_BY_USER_RANGE:
        userid = str(params["userid"])
        from_date = _parse_ymd(str(params["from_date"]))
        to_date = _parse_ymd(str(params["to_date"]))

        if to_date < from_date:
            raise ValueError("to_date debe ser >= from_date")

        # order: si no lo pasan, forzamos workdate asc
        forced_order = str(params.get("order") or "workdate|asc").strip()
        can_cut_early = forced_order.lower() == "workdate|asc"

        pageSize = coerce_page_size(params)

        collected: List[Dict[str, Any]] = []
        page = 1

        # guardrails
        seen_ids: set[str] = set()
        empty_pages = 0
        max_pages = 200

        while True:
            params_list: List[Tuple[str, str]] = [("zone", "timesheets")]

            # filtro por usuario y por fecha mínima
            params_list.append(("where", f"and|userid|=|{userid}"))
            params_list.append(("where", f"and|workdate|>=|{from_date.isoformat()}"))

            # orden para que el corte temprano sea válido (si AroFlo lo respeta)
            if forced_order:
                params_list.append(("order", forced_order))

            if pageSize:
                params_list.append(("pageSize", str(pageSize)))

            params_list.append(("page", str(page)))

            resp = request(client, "GET", params_list)

            # Soporta shapes:
            #  - {"status":..., "zoneresponse": {...}}
            #  - {"data": {"status":..., "zoneresponse": {...}}}
            root = resp if isinstance(resp, dict) else {}
            data = root.get("data") if isinstance(root.get("data"), dict) else root
            zr = data.get("zoneresponse", {}) if isinstance(data, dict) else {}
            items = zr.get("timesheets", []) if isinstance(zr, dict) else []

            if not items:
                empty_pages += 1
                # a veces AroFlo devuelve página vacía intermedia; permitimos 1
                if empty_pages >= 2:
                    break
                page += 1
                if page > max_pages:
                    break
                continue

            empty_pages = 0

            stop = False
            new_ids_in_page = 0

            for ts in items:
                tsid = str(ts.get("timesheetid", "") or "")
                if tsid:
                    if tsid in seen_ids:
                        # Si se repite demasiado, es señal de paginación rota / order ignorado
                        continue
                    seen_ids.add(tsid)
                    new_ids_in_page += 1

                wd = _parse_workdate(str(ts.get("workdate", "")))
                if wd is None:
                    continue

                # Si el API realmente está devolviendo workdate asc, podemos cortar
                if can_cut_early and wd > to_date:
                    stop = True
                    break

                if from_date <= wd <= to_date:
                    collected.append(ts)

            if stop:
                break

            # guardrail: si la página no trajo nada "nuevo", paramos para no loop infinito
            if new_ids_in_page == 0 and page > 1:
                break

            # si devolvió menos que pageSize, probablemente no hay más
            if pageSize and isinstance(items, list) and len(items) < int(pageSize):
                break

            page += 1
            if page > max_pages:
                break

        # (opcional) ordenar localmente por workdate asc para salida estable
        def _wd_key(ts: Dict[str, Any]) -> Tuple[int, int, int]:
            wd = _parse_workdate(str(ts.get("workdate", "")))
            if not wd:
                return (0, 0, 0)
            return (wd.year, wd.month, wd.day)

        collected.sort(key=lambda ts: (str(ts.get("workdate", "")), str(ts.get("timesheetid", ""))))


        out = {
            "data": {
                "status": "0",
                "statusmessage": "OK (range filtered locally)",
                "zoneresponse": {
                    "currentpageresults": len(collected),
                    "timesheets": collected,
                },
            }
        }

        if raw:
            out["meta"] = {
                "strategy": "api where: userid AND workdate>=from_date; local filter: from_date<=workdate<=to_date",
                "userid": userid,
                "from_date": from_date.isoformat(),
                "to_date": to_date.isoformat(),
                "order": forced_order,
                "pageSize": str(pageSize) if pageSize else None,
                "pages_scanned": page,
                "can_cut_early": can_cut_early,
                "unique_timesheetids_seen": len(seen_ids),
            }

        return out

    raise ValueError(f"[Timesheets.base] Operación no soportada: {operation_code}")
