# apps/aroflo_connector_app/ui_automation/flows/timesheet_create.py
from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional, Tuple

from playwright.sync_api import Page, Locator

from ..core.artifacts import shot
from ..core.log import log_step, pause
from ..auth.post_login import handle_terminate_sessions_if_present





@dataclass(frozen=True)
class NewRowSpec:
    hours: str
    overhead: str
    note: Optional[str] = None
    worktype_label: str = "NT"
    tracking_label: str = "ADMIN"


def _close_session_limit_if_any(page: Page, run_dir: Path) -> None:
    try:
        if handle_terminate_sessions_if_present(page, run_dir):
            page.wait_for_timeout(400)
    except Exception:
        pass


def _wait_timesheet_ready(page: Page) -> None:
    page.wait_for_function(
        """() => {
            const u = location.href.toLowerCase();
            if (!u.includes('timesheet')) return false;
            const t = document.body && (document.body.innerText || '');
            return t.includes('Daily Timesheet') || t.includes('Timesheet for');
        }""",
        timeout=45_000,
    )


def _click_save(page: Page) -> None:
    # En AroFlo a veces es input[type=button] verde, a veces button.
    candidates = [
        page.get_by_role("button", name="Save"),
        page.locator("button:has-text('Save')"),
        page.locator("input[type='button'][value='Save']"),
        page.locator("input[type='submit'][value='Save']"),
        page.locator("input.af-add-timesheet-form-btn[value='Save']"),
    ]
    for c in candidates:
        try:
            if c.count() == 0:
                continue
            c.first.click(timeout=7000, force=True)
            return
        except Exception:
            continue
    raise RuntimeError("Save button not found/clickable.")


def _get_new_row(page: Page, n: int) -> Locator:
    # Fila "create" se identifica por input#NTnew{n}
    nt = page.locator(f"input#NTnew{n}")
    if nt.count() == 0:
        raise RuntimeError(f"No encuentro input de creación: input#NTnew{n}")
    return nt.first.locator("xpath=ancestor::tr[contains(@class,'taskRow')]")


def _get_new_row_fields(row: Locator, n: int) -> Tuple[Locator, Locator, Locator, Locator]:
    # Hrs/Qty
    hrs = row.locator(f"input#NTnew{n}").first

    # Overhead (input de búsqueda)
    overhead_inp = row.locator("input.schOverhead").first

    # Note (en newX suele ser name="noteX")
    note = row.locator(f'textarea[name="note{n}"]').first
    if note.count() == 0:
        # fallback: algunos layouts usan textarea.description
        note = row.locator("textarea.description").first

    # Type select (newworktypeX)
    type_sel = row.locator(f'select[name="newworktype{n}"]').first
    if type_sel.count() == 0:
        # fallback: algunos layouts podrían no usar newworktypeX (raro, pero por si acaso)
        type_sel = row.locator("select.worktype").first

    return hrs, overhead_inp, note, type_sel

def _set_overhead_new(page: Page, row: Locator, overhead_text: str) -> None:
    """
    Overhead se asigna SIEMPRE vía el diálogo jqGrid (lupa):
    1) Click lupa (dentro del row).
    2) Espera dialog #dialog_search_overheads.
    3) Escribe en #gs_overhead.
    4) Click en la fila que matchee el overhead.
    5) Click "Select Overhead" (#btnSelect).
    6) Verifica que el input.schOverhead de la fila quedó con el valor esperado.
    """

    inp = row.locator("input.schOverhead").first
    if inp.count() == 0:
        raise RuntimeError("No encuentro input.schOverhead en la fila.")

    
    # Lupa de OVERHEAD: buscar el wrapper que contiene input.schOverhead y dentro su lupa
    search_btn = row.locator(
        "div.imsSearchBox__wrapper:has(input.schOverhead) div#btnAdvSearch"
    ).first

    if search_btn.count() == 0:
        # fallback: usa el sibling directo del input
        search_btn = row.locator("input.schOverhead + div#btnAdvSearch").first

    if search_btn.count() == 0:
        raise RuntimeError("No encuentro la lupa de Overhead (btnAdvSearch asociado a input.schOverhead).")

    search_btn.click(force=True)


    dialog = page.locator("#dialog_search_overheads").first
    dialog.wait_for(state="attached", timeout=7000)
    dialog.wait_for(state="visible", timeout=7000)


    # Input de búsqueda del grid
    q = dialog.locator("input#gs_overhead").first
    q.wait_for(timeout=3000)
    q.click()
    q.fill(overhead_text)

    # jqGrid suele filtrar con keyup / change. Forzamos keyup con un espacio/backspace o Enter.
    q.press("Enter")
    page.wait_for_timeout(350)

    # Seleccionar fila que contenga el texto exacto en la columna overhead
    # (El id de la tabla cambia: tblSearchBox####, así que usamos selector por patrón)
    grid = dialog.locator("table[id^='tblSearchBox']").first
    grid.wait_for(timeout=5000)

    # Celda de overhead: aria-describedby="tblSearchBox3913_overhead" (varía el número)
    cell = grid.locator("tr.jqgrow td[aria-describedby$='_overhead']").filter(has_text=overhead_text).first
    if cell.count() == 0:
        # fallback: coincide parcial
        cell = grid.locator("tr.jqgrow td[aria-describedby$='_overhead']").filter(has_text=overhead_text.split(" ")[0]).first

    if cell.count() == 0:
        # Debug útil: screenshot y error claro
        raise RuntimeError(f"No encuentro overhead '{overhead_text}' en el grid del diálogo.")

    # Click para seleccionar el row
    cell.click()
    page.wait_for_timeout(150)

    # Click "Select Overhead"
    btn_select = dialog.locator("#btnSelect").first
    if btn_select.count() == 0:
        btn_select = dialog.locator("text=Select Overhead").first
    btn_select.click(force=True)

    # Esperar a que el dialog desaparezca
    dialog.wait_for(state="hidden", timeout=7000)

    # Verificación: el input de la fila debe quedar con el overhead
    try:
        final_val = inp.input_value(timeout=1500) or ""
    except Exception:
        final_val = ""

    if overhead_text.lower() not in final_val.lower():
        raise RuntimeError(
            f"Overhead no quedó aplicado. Esperaba '{overhead_text}', quedó '{final_val}'."
        )

def _get_tracking_select(row: Locator, n: int) -> Locator:
    sel = row.locator(f'select[name="new_tracking_centre_id{n}"]').first
    if sel.count() == 0:
        sel = row.locator("select.tracking-centre").first
    return sel


def _is_empty(locator: Locator) -> bool:
    try:
        # Para inputs y textareas
        tag = locator.evaluate("el => el.tagName.toLowerCase()")
        if tag in ("input", "textarea"):
            v = locator.input_value(timeout=500)
            return (v or "").strip() == ""
    except Exception:
        pass
    # Fallback conservador
    return True


def _assert_new_row_empty(page: Page, n: int) -> None:
    row = _get_new_row(page, n)
    hrs, overhead_inp, note, _type_sel = _get_new_row_fields(row, n)

    # Si alguno tiene contenido, NO es "create limpio"
    if not _is_empty(hrs):
        raise RuntimeError(f"Fila new{n} no está vacía: Hrs/Qty ya tiene valor.")
    if overhead_inp.count() and not _is_empty(overhead_inp):
        raise RuntimeError(f"Fila new{n} no está vacía: Overhead ya tiene valor.")
    # Note puede venir con whitespace; sólo falla si hay contenido real
    if note.count():
        try:
            v = (note.input_value(timeout=500) or "").strip()
            if v:
                raise RuntimeError(f"Fila new{n} no está vacía: Note ya tiene valor.")
        except Exception:
            pass


def _select_autocomplete_first(page: Page) -> None:
    # patrón típico: después de escribir, dropdown abre; ArrowDown + Enter elige el primero
    page.wait_for_timeout(200)
    page.keyboard.press("ArrowDown")
    page.keyboard.press("Enter")
    page.wait_for_timeout(200)


def _fill_new_row(page: Page, n: int, spec: NewRowSpec) -> None:
    row = _get_new_row(page, n)

    hrs, overhead_inp, note, type_sel = _get_new_row_fields(row, n)
    tracking_sel = _get_tracking_select(row, n)

    # 1) Hrs/Qty
    hrs.click()
    hrs.fill("")
    hrs.type(str(spec.hours), delay=25)

    # 2) Overhead (modal/lupa)
    _set_overhead_new(page, row, spec.overhead)

    # 3) Note
    # Note (opcional - por ahora lo omitimos)
    if spec.note:
        if note.count() == 0:
            raise RuntimeError(f"Fila new{n}: No encuentro textarea de note.")
        note.click()
        note.fill(spec.note)


    # 4) Type
    if type_sel.count():
        try:
            type_sel.select_option(label=spec.worktype_label)
            type_sel.dispatch_event("change")
        except Exception:
            pass

    # 5) Tracking centre
    if tracking_sel.count():
        try:
            tracking_sel.select_option(label=spec.tracking_label)
            tracking_sel.dispatch_event("change")
        except Exception:
            pass

def _ensure_new_rows_exist(page: Page, n: int) -> None:
    """
    Asegura que existan inputs NTnew1..NTnewN.
    Si AroFlo sólo muestra 1 fila, normalmente hay un botón tipo "Add Row" / "Add".
    Ajusta los selectores de acuerdo al DOM real cuando lo veas en screenshots.
    """
    def exists(i: int) -> bool:
        return page.locator(f"input#NTnew{i}").count() > 0

    if exists(n):
        return

    # Candidatos típicos para "Add Row"
    add_candidates = [
        page.get_by_role("button", name="Add"),
        page.get_by_role("button", name="Add Row"),
        page.locator("button:has-text('Add')"),
        page.locator("a:has-text('Add')"),
        page.locator("input[type='button'][value*='Add']"),
        page.locator("button#afAddTimesheetRow"),
    ]

    for _ in range(10):  # límite defensivo
        if exists(n):
            return
        clicked = False
        for c in add_candidates:
            try:
                if c.count() == 0:
                    continue
                c.first.click(timeout=2000, force=True)
                page.wait_for_timeout(250)
                clicked = True
                break
            except Exception:
                continue
        if not clicked:
            break

    if not exists(n):
        raise RuntimeError(f"No pude asegurar filas new hasta {n}. No existe input#NTnew{n}.")



def run(page: Page, cfg, run_dir: Path, *, rows: Optional[List[NewRowSpec]] = None) -> None:
    """
    SOLO CREATE:
    - Solo escribe en filas newX (NTnew1, NTnew2, ...)
    - Requiere que la fila esté vacía
    - No toca filas existentes (IDs numéricos)
    """
    _close_session_limit_if_any(page, run_dir)
    _wait_timesheet_ready(page)

    _ensure_new_rows_exist(page, len(rows))

    # Default: 3 filas como tu caso (ajústalo cuando quieras)
    if rows is None:
        rows = [
            NewRowSpec(hours="5", overhead="Admin Duties", worktype_label="NT", tracking_label="ADMIN"),
            NewRowSpec(hours="0.5", overhead="Lunch Break - Unpaid", worktype_label="Lunch Break", tracking_label="ADMIN"),
            NewRowSpec(hours="2", overhead="Admin Duties", worktype_label="NT", tracking_label="ADMIN"),
        ]

    shot(page, run_dir, "ts-create-01-ready")
    log_step("ts-create-01-ready", page)
    pause(cfg, "Timesheet ready (before create fill)")

    # Validar y llenar filas new1..newN
    for idx, spec in enumerate(rows, start=1):
        _assert_new_row_empty(page, idx)
        _fill_new_row(page, idx, spec)

    shot(page, run_dir, "ts-create-02-filled")
    log_step("ts-create-02-filled", page)
    pause(cfg, "Filled new rows (before save)")

    _click_save(page)
    page.wait_for_timeout(1500)

    _close_session_limit_if_any(page, run_dir)

    shot(page, run_dir, "ts-create-03-after-save")
    log_step("ts-create-03-after-save", page)
    pause(cfg, "After save")
