from __future__ import annotations

import os

from argparse import Namespace
from pathlib import Path

from flask import Blueprint, request, jsonify, g

from apps.aroflo_connector_app.ui_automation_zones.core.config import build_config
from apps.aroflo_connector_app.ui_automation_zones.core.artifacts import create_run_dir, shot
from apps.aroflo_connector_app.ui_automation_zones.core.runtime import SESSION_POOL
from apps.aroflo_connector_app.ui_automation_zones.auth.detect import is_mfa_screen, is_authenticated
from apps.aroflo_connector_app.ui_automation_zones.zones.timesheets.flows import nav as ts_nav
from apps.aroflo_connector_app.ui_automation_zones.zones.timesheets.flows import select_date as ts_date
from apps.aroflo_connector_app.ui_automation_zones.zones.timesheets.flows import select_user as ts_user
from apps.aroflo_connector_app.ui_automation_zones.zones.timesheets.flows import create as ts_create
from apps.aroflo_connector_app.ui_automation_zones.zones.timesheets.flows import delete as ts_delete
from apps.aroflo_connector_app.ui_automation_zones.zones.timesheets.flows.create import NewRowSpec
from apps.aroflo_connector_app.ui_automation_zones.zones.timesheets.flows.delete import DeleteRule
from apps.aroflo_connector_app.ui_automation_zones.zones.users.flows import nav as users_nav
from apps.aroflo_connector_app.ui_automation_zones.zones.users.flows import select_user as users_select
from apps.aroflo_connector_app.ui_automation_zones.zones.users.flows import upload_docs as users_upload
from apps.aroflo_connector_app.ui_automation_zones.zones.users.flows.upload_docs import DocSpec
from apps.aroflo_connector_app.ui_automation_zones.common.siteadministration.settings.timesheets import overheads as sa_overheads

from apps.aroflo_connector_app.ui_automation_zones.services.session_service import (
    get_status,
    connect_session,
    submit_mfa_code,
    close_session,
)
from apps.aroflo_connector_app.worker_security import authenticate_worker_request
from apps.aroflo_connector_app.bot_leases import acquire_lease, release_lease, update_state
from bridge_platform.secrets.secrets_manager import SecretsManager

ui_worker_bp = Blueprint("ui_automation_worker", __name__, url_prefix="/ui")


@ui_worker_bp.before_request
def require_internal_auth():
    data = request.args if request.method == "GET" else (request.get_json(silent=True) or {})
    tenant_id = _tenant_from(data)
    bot_id = str(data.get("bot_id") or data.get("bot-id") or "").strip().lower()
    if tenant_id == "<default>" or not bot_id:
        return jsonify({"status": "denied", "message": "tenant_id and bot_id are required"}), 400
    try:
        authenticate_worker_request(request, tenant_id)
    except PermissionError as exc:
        return jsonify({"status": "denied", "message": str(exc)}), 401
    g.aroflo_tenant_id = tenant_id
    g.aroflo_bot_id = bot_id
    g.aroflo_session_key = f"{tenant_id}:{bot_id}"
    g.aroflo_lease_owner = f"{os.getenv('AROFLO_UI_WORKER_ID', 'worker')}:{request.headers.get('X-AroFlo-Nonce', '')}"
    if request.method != "GET":
        try:
            acquire_lease(tenant_id, bot_id, owner=g.aroflo_lease_owner, operation=request.path)
        except RuntimeError:
            return jsonify({"status": "busy", "message": "BOT is processing another operation"}), 409


@ui_worker_bp.teardown_request
def release_internal_lease(_error):
    if request.method == "GET" or not getattr(g, "aroflo_lease_owner", None):
        return
    release_lease(g.aroflo_tenant_id, g.aroflo_bot_id, owner=g.aroflo_lease_owner)


def _tenant_from(data: dict | None) -> str:
    if getattr(g, "aroflo_session_key", None):
        return g.aroflo_session_key
    data = data or {}
    tenant_id = (data.get("tenant_id") or data.get("tenant-id") or "").strip()
    return tenant_id or "<default>"


def _is_allowed_tenant(tenant_id: str) -> bool:
    tenant_id = tenant_id.split(":", 1)[0]
    allowed = (os.getenv("AROFLO_UI_ALLOWED_TENANT_ID") or "").strip()
    if not allowed:
        # default abierto si no se configura
        return True
    if allowed == "<default>":
        return tenant_id == "<default>"
    return tenant_id == allowed


def _deny_if_not_allowed(tenant_id: str):
    if getattr(g, "aroflo_tenant_id", None):
        return None
    if not _is_allowed_tenant(tenant_id):
        return jsonify({"status": "denied", "message": "tenant_id not allowed"}), 403
    return None


def _build_cfg_for_tenant(tenant_id: str):
    real_tenant_id, separator, bot_id = tenant_id.partition(":")
    if not separator or not bot_id:
        raise ValueError("A tenant and BOT session key is required")
    manager = SecretsManager()
    def secret(suffix: str) -> str:
        return manager.get_secret(tenant_id=real_tenant_id, app_id="aroflo_connector_app",
                                  secret_name=f"bot_{bot_id}_{suffix}") or ""
    args = Namespace(
        base_url=secret("base_url"),
        username=secret("username"),
        password=secret("password"),
        tenant_id=tenant_id,
        reuse_session=True,
        keep_open=True,
        state_file=None,
        artifacts_dir=None,
        headless=None,
        slow_mo_ms=None,
        step=False,
        pause_on_mfa=False,
        mfa_code="",

        user_email="",
        doc=[],
        file=[],
        comment=[],
        filter=[],

        timesheet_date="",
        timesheet_bu="",
        timesheet_user_id="",
        timesheet_user_name="",
        row=[],
        delete_all=False,
        include_protected=False,
    )
    return build_config(args)


def _get_runtime_or_error(tenant_id: str):
    rt = SESSION_POOL._sessions.get(tenant_id)
    if not rt:
        return None, (jsonify({"state": "OFFLINE", "tenant_id": tenant_id}), 409)
    try:
        if is_mfa_screen(rt.page):
            return None, (jsonify({"state": "MFA_REQUIRED", "tenant_id": tenant_id}), 409)
        if not is_authenticated(rt.page):
            return None, (jsonify({"state": "OFFLINE", "tenant_id": tenant_id}), 409)
    except Exception:
        return None, (jsonify({"state": "UNKNOWN", "tenant_id": tenant_id}), 409)
    return rt, None


def _public_session_result(result: dict) -> dict:
    """Keep the internal tenant:bot session key out of the public contract."""
    return {
        **result,
        "tenant_id": g.aroflo_tenant_id,
        "bot_id": g.aroflo_bot_id,
    }


@ui_worker_bp.get("/session/status")
def session_status():
    tenant_id = g.aroflo_session_key
    denied = _deny_if_not_allowed(tenant_id)
    if denied:
        return denied
    return jsonify(_public_session_result(get_status(tenant_id=tenant_id)))


@ui_worker_bp.post("/session/connect")
def session_connect():
    data = request.get_json(silent=True) or {}
    tenant_id = g.aroflo_session_key
    denied = _deny_if_not_allowed(tenant_id)
    if denied:
        return denied
    out = connect_session(tenant_id=tenant_id)
    update_state(g.aroflo_tenant_id, g.aroflo_bot_id, state=out.get("state", "UNKNOWN").lower(),
                 worker_id=os.getenv("AROFLO_UI_WORKER_ID", "worker"))
    return jsonify(_public_session_result(out))


@ui_worker_bp.post("/session/mfa")
def session_mfa():
    data = request.get_json(silent=True) or {}
    tenant_id = g.aroflo_session_key
    denied = _deny_if_not_allowed(tenant_id)
    if denied:
        return denied
    code = (data.get("code") or "").strip()
    if not code:
        return jsonify({"status": "error", "message": "Missing MFA code"}), 400
    out = submit_mfa_code(
        tenant_id=tenant_id,
        code=code,
        remember_30_days=bool(data.get("remember_30_days")),
    )
    update_state(g.aroflo_tenant_id, g.aroflo_bot_id, state=out.get("state", "UNKNOWN").lower(),
                 worker_id=os.getenv("AROFLO_UI_WORKER_ID", "worker"))
    return jsonify(_public_session_result(out))


@ui_worker_bp.post("/session/close")
def session_close():
    data = request.get_json(silent=True) or {}
    tenant_id = g.aroflo_session_key
    denied = _deny_if_not_allowed(tenant_id)
    if denied:
        return denied
    out = close_session(tenant_id=tenant_id)
    update_state(g.aroflo_tenant_id, g.aroflo_bot_id, state="offline",
                 worker_id=os.getenv("AROFLO_UI_WORKER_ID", "worker"))
    return jsonify(_public_session_result(out))


@ui_worker_bp.post("/timesheets/create")
def timesheets_create():
    data = request.get_json(silent=True) or {}
    tenant_id = _tenant_from(data)
    denied = _deny_if_not_allowed(tenant_id)
    if denied:
        return denied

    rt, err = _get_runtime_or_error(tenant_id)
    if err:
        return err

    cfg = _build_cfg_for_tenant(tenant_id)
    run_dir = create_run_dir(cfg, "worker-timesheets-create")

    cfg = cfg.__class__(
        **{**cfg.__dict__,
           "timesheet_bu": (data.get("timesheet_bu") or data.get("timesheet-bu") or "").strip(),
           "timesheet_date": (data.get("timesheet_date") or data.get("timesheet-date") or "").strip(),
           "timesheet_user_id": (data.get("timesheet_user_id") or data.get("timesheet-user-id") or "").strip(),
           "timesheet_user_name": (data.get("timesheet_user_name") or data.get("timesheet-user-name") or "").strip(),
        }
    )

    rows_raw = data.get("rows") or data.get("row") or []
    rows = None
    if rows_raw:
        parsed = []
        for spec in rows_raw:
            if isinstance(spec, dict):
                parsed.append(spec)
                continue
            parts = [p.strip() for p in str(spec).split(";") if p.strip()]
            out = {}
            for part in parts:
                if "=" not in part:
                    return jsonify({"status": "error", "message": f"Invalid row token: {part!r}"}), 400
                k, v = part.split("=", 1)
                out[k.strip().lower()] = v.strip()
            if not out.get("hours") or not out.get("overhead"):
                return jsonify({"status": "error", "message": f"--row missing hours/overhead: {spec}"}), 400
            out.setdefault("worktype", "NT")
            out.setdefault("tracking", "ADMIN")
            out.setdefault("note", "")
            parsed.append(out)
        rows = [
            NewRowSpec(
                hours=r["hours"],
                overhead=r["overhead"],
                note=(r.get("note") or None),
                worktype_label=r.get("worktype", "NT"),
                tracking_label=r.get("tracking", "ADMIN"),
            )
            for r in parsed
        ]

    lock = SESSION_POOL.tenant_lock(tenant_id)
    with lock:
        try:
            ts_nav.run(rt.page, cfg, run_dir)
            if cfg.timesheet_user_id or cfg.timesheet_user_name:
                ts_user.run(
                    rt.page,
                    cfg,
                    run_dir,
                    user_id=cfg.timesheet_user_id,
                    user_name=cfg.timesheet_user_name,
                )
            if cfg.timesheet_date:
                ts_date.run(rt.page, cfg, run_dir, target_date=cfg.timesheet_date)
            ts_create.run(rt.page, cfg, run_dir, rows=rows)
            shot(rt.page, run_dir, "ok")
            rt.save_state()
            rt.touch()
            return jsonify({"status": "ok", "tenant_id": tenant_id})
        except Exception as e:
            try:
                shot(rt.page, run_dir, "error")
            except Exception:
                pass
            return jsonify({"status": "error", "message": str(e), "tenant_id": tenant_id}), 500


@ui_worker_bp.post("/timesheets/delete")
def timesheets_delete():
    data = request.get_json(silent=True) or {}
    tenant_id = _tenant_from(data)
    denied = _deny_if_not_allowed(tenant_id)
    if denied:
        return denied

    rt, err = _get_runtime_or_error(tenant_id)
    if err:
        return err

    cfg = _build_cfg_for_tenant(tenant_id)
    run_dir = create_run_dir(cfg, "worker-timesheets-delete")

    cfg = cfg.__class__(
        **{**cfg.__dict__,
           "timesheet_bu": (data.get("timesheet_bu") or data.get("timesheet-bu") or "").strip(),
           "timesheet_date": (data.get("timesheet_date") or data.get("timesheet-date") or "").strip(),
           "timesheet_user_id": (data.get("timesheet_user_id") or data.get("timesheet-user-id") or "").strip(),
           "timesheet_user_name": (data.get("timesheet_user_name") or data.get("timesheet-user-name") or "").strip(),
           "delete_all": bool(data.get("delete_all", False)),
           "include_protected": bool(data.get("include_protected", False)),
        }
    )

    rule = DeleteRule(
        overheads_to_delete=[
            "Admin Duties",
            "Admin Duties - Telecommunications",
        ],
        protected_overheads=[
            "Lunch Break - Unpaid",
        ],
        match_mode="exact",
        delete_all=cfg.delete_all,
        include_protected=cfg.include_protected,
    )

    lock = SESSION_POOL.tenant_lock(tenant_id)
    with lock:
        try:
            ts_nav.run(rt.page, cfg, run_dir)
            if cfg.timesheet_user_id or cfg.timesheet_user_name:
                ts_user.run(
                    rt.page,
                    cfg,
                    run_dir,
                    user_id=cfg.timesheet_user_id,
                    user_name=cfg.timesheet_user_name,
                )
            if cfg.timesheet_date:
                ts_date.run(rt.page, cfg, run_dir, target_date=cfg.timesheet_date)
            ts_delete.run(rt.page, cfg, run_dir, rule=rule)
            shot(rt.page, run_dir, "ok")
            rt.save_state()
            rt.touch()
            return jsonify({"status": "ok", "tenant_id": tenant_id})
        except Exception as e:
            try:
                shot(rt.page, run_dir, "error")
            except Exception:
                pass
            return jsonify({"status": "error", "message": str(e), "tenant_id": tenant_id}), 500


@ui_worker_bp.post("/users/upload-docs")
def users_upload_docs():
    data = request.get_json(silent=True) or {}
    tenant_id = _tenant_from(data)
    denied = _deny_if_not_allowed(tenant_id)
    if denied:
        return denied

    rt, err = _get_runtime_or_error(tenant_id)
    if err:
        return err

    cfg = _build_cfg_for_tenant(tenant_id)
    run_dir = create_run_dir(cfg, "worker-users-upload-docs")

    cfg = cfg.__class__(
        **{**cfg.__dict__,
           "timesheet_bu": (data.get("timesheet_bu") or data.get("timesheet-bu") or "").strip(),
           "user_email": (data.get("user_email") or data.get("user-email") or "").strip(),
        }
    )

    docs_raw = data.get("docs") or []
    if not isinstance(docs_raw, list) or not docs_raw:
        return jsonify({"status": "error", "message": "docs is required"}), 400

    docs: list[DocSpec] = []
    for item in docs_raw:
        if isinstance(item, dict):
            f = (item.get("file") or "").strip()
            if not f:
                return jsonify({"status": "error", "message": "doc missing file"}), 400
            docs.append(DocSpec(file=f, comment=item.get("comment", ""), filter=item.get("filter", "")))
            continue
        parts = [p.strip() for p in str(item).split(";") if p.strip()]
        kv = {}
        for part in parts:
            if "=" not in part:
                return jsonify({"status": "error", "message": f"Invalid doc token: {part!r}"}), 400
            k, v = part.split("=", 1)
            kv[k.strip().lower()] = v.strip()
        if not kv.get("file"):
            return jsonify({"status": "error", "message": f"--doc missing file= in {item!r}"}), 400
        docs.append(DocSpec(file=kv["file"], comment=kv.get("comment", ""), filter=kv.get("filter", "")))

    lock = SESSION_POOL.tenant_lock(tenant_id)
    with lock:
        try:
            users_nav.run(rt.page, cfg, run_dir, select_user=False)
            result = users_select.select_user_by_email(rt.page, cfg.user_email, run_dir=run_dir)
            if not result.found:
                return jsonify({"status": "error", "message": "user not found", "tenant_id": tenant_id}), 404
            users_upload.users_upload_documents(rt.page, docs=docs, cfg=cfg, run_dir=run_dir, timeout_ms=45_000)
            shot(rt.page, run_dir, "ok")
            rt.save_state()
            rt.touch()
            return jsonify({"status": "ok", "tenant_id": tenant_id})
        except Exception as e:
            try:
                shot(rt.page, run_dir, "error")
            except Exception:
                pass
            return jsonify({"status": "error", "message": str(e), "tenant_id": tenant_id}), 500


@ui_worker_bp.post("/overheads/refresh")
def overheads_refresh():
    data = request.get_json(silent=True) or {}
    tenant_id = _tenant_from(data)
    denied = _deny_if_not_allowed(tenant_id)
    if denied:
        return denied

    rt, err = _get_runtime_or_error(tenant_id)
    if err:
        return err

    cfg = _build_cfg_for_tenant(tenant_id)
    run_dir = create_run_dir(cfg, "worker-overheads-refresh")

    lock = SESSION_POOL.tenant_lock(tenant_id)
    with lock:
        try:
            items = sa_overheads.run(rt.page, cfg, run_dir)
            shot(rt.page, run_dir, "ok")
            rt.save_state()
            rt.touch()
            return jsonify({"status": "ok", "tenant_id": tenant_id, "count": len(items or [])})
        except Exception as e:
            try:
                shot(rt.page, run_dir, "error")
            except Exception:
                pass
            return jsonify({"status": "error", "message": str(e), "tenant_id": tenant_id}), 500
