from __future__ import annotations

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

from playwright.sync_api import Page

from ..core.artifacts import shot
from ..core.log import log_step, pause


@dataclass
class UploadResult:
    ok: bool
    filename: str
    reason: str = ""


@dataclass(frozen=True)
class DocSpec:
    file: str | Path
    comment: str = ""
    filter: str = ""


# -------------------------
# Helpers base
# -------------------------
def _first_any(locators):
    for loc in locators:
        try:
            if loc and loc.count() > 0:
                return loc.first
        except Exception:
            continue
    return None


def _wait_any_selector(page: Page, selectors: Sequence[str], *, timeout_ms: int, state: str = "visible") -> None:
    last_err: Optional[Exception] = None
    for sel in selectors:
        try:
            page.wait_for_selector(sel, timeout=timeout_ms, state=state)
            return
        except Exception as e:
            last_err = e
            continue
    raise RuntimeError(f"None of the expected selectors appeared: {selectors!r} (state={state})") from last_err


# -------------------------
# Documents tab
# -------------------------
def users_open_documents_tab(page: Page, *, timeout_ms: int = 20_000) -> None:
    candidates = [
        page.locator('a.extTabLink.documenttab[href="#documenttab"]'),
        page.locator('a.extTabLink[href="#documenttab"]'),
        page.locator('a:has-text("Documents & Photos")'),
        page.locator('a:has-text("Documents")'),
        page.get_by_role("link", name="Documents"),
    ]
    tab = _first_any(candidates)
    if not tab:
        raise RuntimeError("Documents tab not found (no matching selectors).")

    tab.click(timeout=timeout_ms)
    page.wait_for_timeout(300)

    _wait_any_selector(
        page,
        selectors=["#documenttab", "button#btnUploadNew", "#dlgUploadNew"],
        timeout_ms=timeout_ms,
        state="attached",
    )


# -------------------------
# Upload modal (determinista)
# -------------------------
def _open_upload_modal(page: Page, *, timeout_ms: int = 20_000) -> None:
    btn = _first_any(
        [
            page.locator("button#btnUploadNew"),
            page.locator('button:has-text("Add Documents & Photos")'),
            page.locator('button:has-text("Add Documents")'),
            page.locator('a:has-text("Add Documents & Photos")'),
            page.locator('a:has-text("Add Documents")'),
        ]
    )
    if not btn:
        raise RuntimeError("Upload button not found (Add Documents & Photos).")

    btn.click(timeout=timeout_ms)
    page.wait_for_timeout(250)

    page.wait_for_selector("#dlgUploadNew", timeout=timeout_ms, state="visible")
    page.wait_for_selector("#dlgUploadNew #s3FileUpload", timeout=timeout_ms, state="attached")


def _choose_file_in_modal(page: Page, file_path: Path, *, timeout_ms: int = 30_000) -> None:
    page.wait_for_selector("#dlgUploadNew", timeout=timeout_ms, state="visible")

    inp = page.locator("#dlgUploadNew input#s3FileUpload")
    inp.wait_for(state="attached", timeout=timeout_ms)

    inp.set_input_files(str(file_path), timeout=timeout_ms)
    page.wait_for_timeout(600)


def _close_modal_done(page: Page, *, timeout_ms: int = 10_000) -> None:
    page.wait_for_selector("#dlgUploadNew", timeout=timeout_ms, state="visible")

    done_btn = page.locator(
        'div.ui-dialog:has(#dlgUploadNew) .ui-dialog-buttonpane .ui-dialog-buttonset button:has-text("Done")'
    )
    if done_btn.count() == 0:
        raise RuntimeError("Done button not found inside upload modal.")

    done_btn.first.click(timeout=timeout_ms)
    page.wait_for_selector("#dlgUploadNew", state="hidden", timeout=timeout_ms)

    try:
        overlay = page.locator("div.ui-widget-overlay")
        if overlay.count() > 0:
            overlay.first.wait_for(state="hidden", timeout=timeout_ms)
    except Exception:
        pass

    page.wait_for_timeout(250)


# -------------------------
# Comment + Filter mapping
# -------------------------
def _normalize_filter_label(s: str) -> str:
    return " ".join((s or "").strip().split()).lower()


FILTER_LABELS = {
    "internal only": "Internal Only",
    "internal admin only(legacy)": "Internal Admin Only (Legacy)",
    "internal admin only (legacy)": "Internal Admin Only (Legacy)",
    "internal admin and manager only(legacy)": "Internal Admin and Manager Only (Legacy)",
    "internal admin and manager only (legacy)": "Internal Admin and Manager Only (Legacy)",
    "show client": "Show Client",
    "show contractor": "Show Contractor",
    "show all": "Show All",
}


def _wait_upload_visible_in_list(page: Page, filename: str, *, timeout_ms: int = 45_000) -> str:
    stem = Path(filename).stem

    grid_root = page.locator("#documenttab").first
    grid_root.wait_for(state="attached", timeout=timeout_ms)

    # jqGrid wrapper visible (esto sí refleja “visible” real)
    grid_root.locator('div[id^="gbox_tblSearchBox"]').first.wait_for(state="visible", timeout=timeout_ms)

    # la tabla puede estar hidden, pero debe existir
    grid_root.locator('table[id^="tblSearchBox"]').first.wait_for(state="attached", timeout=timeout_ms)

    _wait_documents_grid_idle(page, timeout_ms=timeout_ms)

    # 1) exact match
    exact_link = grid_root.locator(
        f'td[aria-describedby*="_document"] a:has-text("{filename}")'
    ).first
    try:
        exact_link.wait_for(state="attached", timeout=timeout_ms)
        return filename
    except Exception:
        pass

    # 2) fallback por stem (detecta "(1)", "(2)", etc)
    stem_link = grid_root.locator(
        f'td[aria-describedby*="_document"] a:has-text("{stem}")'
    ).last
    stem_link.wait_for(state="attached", timeout=timeout_ms)

    real_name = (stem_link.inner_text() or "").strip()
    return real_name or filename


def _find_document_row_by_filename(page: Page, filename: str):
    link = page.locator('table[id^="tblSearchBox"] td[aria-describedby*="_document"] a').filter(has_text=filename)
    if link.count() == 0:
        raise RuntimeError(f"Could not find document link by filename: {filename!r}")

    return link.last.locator("xpath=ancestor::tr[contains(@class,'jqgrow')]")


def _extract_document_id_from_row(row) -> str:
    el = row.locator("[data-documentid]").first
    if el.count() == 0:
        raise RuntimeError("Could not find any element with data-documentid in the document row.")
    doc_id = el.get_attribute("data-documentid")
    if not doc_id:
        raise RuntimeError("data-documentid attribute is empty.")
    return doc_id


def _set_comment_and_filter_for_doc(
    page: Page,
    doc_id: str,
    comment: str,
    filter_label: str,
    *,
    timeout_ms: int = 20_000,
) -> None:
    if comment:
        ta = page.locator(f'textarea[name="docComment{doc_id}"][data-documentid="{doc_id}"]')
        ta.wait_for(state="attached", timeout=timeout_ms)
        ta.fill(comment)
        ta.blur()
        page.dispatch_event(f'textarea[name="docComment{doc_id}"]', "input")
        page.dispatch_event(f'textarea[name="docComment{doc_id}"]', "change")
        page.wait_for_timeout(250)

    if filter_label:
        key = _normalize_filter_label(filter_label)
        if key not in FILTER_LABELS:
            raise RuntimeError(
                f"Invalid filter value: {filter_label!r}. Allowed: {sorted(set(FILTER_LABELS.values()))}"
            )

        sel = page.locator(f'select#Documentfilter{doc_id}[data-documentid="{doc_id}"]')
        sel.wait_for(state="attached", timeout=timeout_ms)
        sel.select_option(label=FILTER_LABELS[key])
        sel.blur()
        page.dispatch_event(f'select#Documentfilter{doc_id}', "change")
        page.wait_for_timeout(250)


def _dismiss_open_menus(page: Page, *, timeout_ms: int = 2_000) -> None:
    try:
        page.keyboard.press("Escape")
        page.wait_for_timeout(150)
    except Exception:
        pass

    try:
        open_menu = page.locator("ul.ui-menu:visible")
        if open_menu.count() > 0:
            page.locator("body").click(position={"x": 5, "y": 5}, timeout=timeout_ms, force=True)
            page.wait_for_timeout(150)
    except Exception:
        pass


def _wait_documents_grid_idle(page: Page, *, timeout_ms: int = 20_000) -> None:
    overlay = page.locator('div.ui-widget-overlay.jqgrid-overlay[id^="lui_tblSearchBox"]')
    loader = page.locator('div.loading[id^="load_tblSearchBox"]')

    try:
        if overlay.count() > 0:
            overlay.first.wait_for(state="hidden", timeout=timeout_ms)
    except Exception:
        pass

    try:
        if loader.count() > 0:
            loader.first.wait_for(state="hidden", timeout=timeout_ms)
    except Exception:
        pass

    page.wait_for_timeout(150)


def _click_save_if_present(page: Page, *, timeout_ms: int = 15_000) -> None:
    save_docs = page.locator("#btnSaveDocuments").first
    if save_docs.count() > 0:
        save_docs.scroll_into_view_if_needed()
        save_docs.click(timeout=timeout_ms, force=True)
        page.wait_for_timeout(800)
        try:
            page.wait_for_load_state("networkidle", timeout=timeout_ms)
        except Exception:
            pass
        return


# -------------------------
# Main flow (DocSpec)
# -------------------------
def users_upload_documents(
    page: Page,
    *,
    docs: List[DocSpec],
    cfg=None,
    run_dir: Path | None = None,
    timeout_ms: int = 45_000,
) -> List[UploadResult]:

    # valida paths
    file_paths: List[Path] = []
    for d in docs:
        p = Path(d.file).expanduser().resolve()
        if not p.exists():
            raise FileNotFoundError(f"Upload file does not exist: {p}")
        file_paths.append(p)

    users_open_documents_tab(page, timeout_ms=timeout_ms)
    if run_dir:
        shot(page, run_dir, "user-docs-01-tab-open")
        log_step("user-docs-01-tab-open", page)
        if cfg:
            pause(cfg, "Documents tab opened")

    results: List[UploadResult] = []

    for idx, (doc, p) in enumerate(zip(docs, file_paths), start=1):
        filename = p.name
        try:
            _open_upload_modal(page, timeout_ms=timeout_ms)
            if run_dir:
                shot(page, run_dir, f"user-docs-02-upload-modal-{idx:02d}")
                log_step(f"user-docs-02-upload-modal-{idx:02d}", page)

            _choose_file_in_modal(page, p, timeout_ms=timeout_ms)
            if run_dir:
                shot(page, run_dir, f"user-docs-03-file-selected-{idx:02d}-{filename}")
                log_step(f"user-docs-03-file-selected-{idx:02d}-{filename}", page)

            _close_modal_done(page, timeout_ms=10_000)
            _dismiss_open_menus(page)
            _wait_documents_grid_idle(page, timeout_ms=timeout_ms)
            if run_dir:
                shot(page, run_dir, f"user-docs-04-modal-closed-{idx:02d}-{filename}")
                log_step(f"user-docs-04-modal-closed-{idx:02d}-{filename}", page)

            # este es el fix del "(1)"
            real_filename = _wait_upload_visible_in_list(page, filename, timeout_ms=timeout_ms)

            _wait_documents_grid_idle(page, timeout_ms=timeout_ms)
            row = _find_document_row_by_filename(page, real_filename)
            row.wait_for(state="attached", timeout=timeout_ms)
            row.scroll_into_view_if_needed()
            page.wait_for_timeout(200)
            doc_id = _extract_document_id_from_row(row)

            _set_comment_and_filter_for_doc(
                page,
                doc_id,
                doc.comment or "",
                doc.filter or "",
                timeout_ms=timeout_ms,
            )

            # 6.5) Guardar inmediatamente para que jqGrid refresh no borre cambios previos
            if doc.comment or doc.filter:
                _dismiss_open_menus(page)
                _wait_documents_grid_idle(page, timeout_ms=timeout_ms)
                _click_save_if_present(page, timeout_ms=15_000)
                _dismiss_open_menus(page)
                _wait_documents_grid_idle(page, timeout_ms=timeout_ms)


            if run_dir:
                shot(page, run_dir, f"user-docs-05-meta-set-{idx:02d}-{real_filename}")
                log_step(f"user-docs-05-meta-set-{idx:02d}-{real_filename}", page)

            results.append(UploadResult(ok=True, filename=real_filename))

        except Exception as e:
            if run_dir:
                shot(page, run_dir, f"user-docs-ERR-{idx:02d}-{filename}")
                log_step(f"user-docs-ERR-{idx:02d}-{filename}", page)
            results.append(UploadResult(ok=False, filename=filename, reason=str(e)))

    any_ok = any(r.ok for r in results)
    any_meta = any((d.comment or "").strip() or (d.filter or "").strip() for d in docs)

    if any_ok and any_meta:
        _dismiss_open_menus(page)
        _wait_documents_grid_idle(page, timeout_ms=timeout_ms)
        if run_dir:
            shot(page, run_dir, "user-docs-89-before-save")
            log_step("user-docs-89-before-save", page)
        _click_save_if_present(page, timeout_ms=15_000)
        _dismiss_open_menus(page)
        _wait_documents_grid_idle(page, timeout_ms=timeout_ms)
        if run_dir:
            shot(page, run_dir, "user-docs-90-save-clicked")
            log_step("user-docs-90-save-clicked", page)

    return results
