from __future__ import annotations

import json
import os
import ssl
import time
import uuid
import urllib.error
import urllib.request
import urllib.parse
from typing import Any, Optional

from worklib.auth_as_employer import get_access_token


DEFAULT_COMPANY_ID = "65437401ae3af6f8ffcdbaf8"
DEFAULT_GRAPHQL_URL = "https://admin.dev.dipal.ru/graphql"

_PASSREQUESTS_MOCK_STATE: dict[str, Any] = {}


def reset_passrequests_mock_state() -> None:
    _PASSREQUESTS_MOCK_STATE.clear()


def _build_ssl_context(url: str) -> ssl.SSLContext | None:
    """
    На dev-стендах сертификат иногда не проходит системную валидацию на Windows.
    По умолчанию для *.dev.dipal.ru отключаем verify, но это можно переопределить env SSL_VERIFY=1.
    """
    try:
        parsed = urllib.parse.urlparse(url)
    except Exception:
        return None

    if (os.getenv("SSL_VERIFY") or "").lower() in {"1", "true", "yes"}:
        return None

    if (parsed.scheme or "").lower() != "https":
        return None

    host = (parsed.hostname or "").lower()
    if host.endswith(".dev.dipal.ru") or host == "dev.dipal.ru":
        return ssl._create_unverified_context()  # noqa: SLF001

    return None


def _mock_passrequests_execute_graphql(*, query: str, variables: Optional[dict[str, Any]], company_id: str, access_token: str) -> dict[str, Any]:
    state = _PASSREQUESTS_MOCK_STATE.setdefault(
        company_id,
        {"places": {}, "users": {}, "passes": {}, "pass_requests_by_pass_id": {}, "members_by_place": {}},
    )

    q = query or ""
    vars_ = variables or {}

    def _new_id(prefix: str) -> str:
        return f"{prefix}_{uuid.uuid4().hex[:12]}"

    # createPlaceMultiple -> [{id}]
    if "createPlaceMultiple" in q:
        place_id = _new_id("place")
        parent_id = None
        if isinstance(vars_, dict):
            parent_id = vars_.get("parent_id")
        state["places"][place_id] = {"id": place_id, "parent_id": parent_id}
        return {"data": {"createPlaceMultiple": [{"id": place_id, "__typename": "Place"}]}}
    # createEntrance -> JSONObject (mocked as object with id)
    if "createEntrance" in q:
        entrance_id = _new_id("entrance")
        return {"data": {"createEntrance": {"id": entrance_id}}}


    # createUser -> id
    if "createUser" in q:
        account_id = _new_id("user")
        state["users"][account_id] = {"id": account_id}
        return {"data": {"createUser": account_id}}

    # createService -> {id,title,type}
    if "createService" in q:
        service_id = _new_id("service")
        title = (vars_.get("title") or "mock-service") if isinstance(vars_, dict) else "mock-service"
        stype = (vars_.get("type") or "access") if isinstance(vars_, dict) else "access"
        return {"data": {"createService": {"id": service_id, "title": title, "type": stype}}}

    # addPlaceToService/removePlaceFromService -> {id}
    if "addPlaceToService" in q or "removePlaceFromService" in q:
        return {"data": {"addPlaceToService": {"id": "ok"}, "removePlaceFromService": {"id": "ok"}}}

    # addUserToPlace(dto: $input) -> {place_id, member_id}
    if "addUserToPlace" in q:
        inp = vars_.get("input") or {}
        place_id = inp.get("place_id")
        account_id = inp.get("account_id")
        if not place_id or not account_id:
            return {"errors": [{"message": "Bad input", "status": 400}]}
        member_id = _new_id("member")
        members = state["members_by_place"].setdefault(str(place_id), [])
        raw_privs = inp.get("privileges") or inp.get("privilege") or []
        if isinstance(raw_privs, str):
            privs = [raw_privs]
        elif isinstance(raw_privs, list):
            privs = raw_privs
        else:
            privs = []
        members.append(
            {
                "id": member_id,
                "status": "accepted",
                "privileges": privs,
                "user": {"id": str(account_id)},
            }
        )
        return {"data": {"addUserToPlace": {"place_id": place_id, "member_id": member_id}}}

    # createPass -> {id}
    if "createPass" in q:
        pass_id = _new_id("pass")
        place_id = None
        if isinstance(vars_, dict):
            place_id = vars_.get("place_id") or (vars_.get("dto") or {}).get("place_id")
        state["passes"][pass_id] = {"id": pass_id, "place_id": place_id}
        pr_id = _new_id("passreq")
        parent_place_id = None
        if place_id and isinstance(state.get("places"), dict):
            parent_place_id = (state["places"].get(str(place_id)) or {}).get("parent_id")
        state["pass_requests_by_pass_id"][pass_id] = {
            "id": pr_id,
            "status": "pending",
            "pass_id": pass_id,
            "confirmer_ids": [],
            "created_at": str(int(time.time())),
            "updated_at": str(int(time.time())),
            "place_id": parent_place_id or place_id or "mock_parent_place",
        }
        return {"data": {"createPass": {"id": pass_id}}}

    # passRequests(filters:{pass_id}) -> results[{...}]
    if "passRequests" in q:
        pass_id = vars_.get("pass_id")
        pr = state["pass_requests_by_pass_id"].get(pass_id)
        if not pr:
            return {"data": {"passRequests": {"results": []}}}
        return {
            "data": {
                "passRequests": {
                    "results": [
                        {
                            "id": pr["id"],
                            "status": pr["status"],
                            "pass_id": pr["pass_id"],
                            "place_id": pr.get("place_id") or "mock_parent_place",
                            "created_at": pr["created_at"],
                            "updated_at": pr["updated_at"],
                            "place": {"id": pr.get("place_id") or "mock_parent_place"},
                            "confirmer_ids": pr["confirmer_ids"],
                        }
                    ]
                }
            }
        }

    # members(filters:{place_id}) -> results[{id,status,privileges,user{id}}]
    # Важно: place(...) query тоже выбирает поле members, поэтому матчим только конкретные members-запросы.
    if ("membersByPlace" in q or "query members" in q) and "members(" in q:
        place_id = None
        if isinstance(vars_, dict):
            place_id = vars_.get("place_id")
        results = state["members_by_place"].get(str(place_id), []) if place_id else []
        return {"data": {"members": {"results": results}}}

    # setUserPlaces -> {id}
    if "setUserPlaces" in q:
        dto = None
        if isinstance(vars_, dict):
            dto = vars_.get("dto") or vars_
        account_id = (dto or {}).get("account_id")
        place_ids = (dto or {}).get("place_ids") or []
        extra_privileges = (dto or {}).get("extra_privileges") or []
        for pid in place_ids:
            members = state["members_by_place"].setdefault(str(pid), [])
            members.append(
                {
                    "id": _new_id("member"),
                    "status": "accepted",
                    "privileges": extra_privileges,
                    "user": {"id": str(account_id)},
                }
            )
        return {"data": {"setUserPlaces": {"id": "ok"}}}

    # place(filters:{member_ids/member_id/user_ids}) -> results[{id,members{...}}]
    if "place(" in q and "filters" in q:
        member_ids = []
        if isinstance(vars_, dict):
            member_ids = vars_.get("member_ids") or vars_.get("user_ids") or []
            mid = vars_.get("member_id")
            if mid:
                member_ids = [mid]
        rows = []
        for pid, members in (state.get("members_by_place") or {}).items():
            if member_ids:
                found = False
                for m in members:
                    u = (m or {}).get("user") if isinstance(m, dict) else None
                    if isinstance(u, dict) and u.get("id") in member_ids:
                        found = True
                        break
                if not found:
                    continue
            rows.append({"id": pid, "members": members})
        return {"data": {"place": {"results": rows}}}

    # approve/confirm -> if already rejected then no-op; else require 2 confirmations to become active
    if "approvePassRequest" in q or "confirmPassRequest" in q:
        pr_id = vars_.get("pass_request_id") or vars_.get("id")
        pr = next((v for v in state["pass_requests_by_pass_id"].values() if v["id"] == pr_id), None)
        if pr and pr["status"] != "rejected":
            conf_id = access_token or "mock_employee"
            if conf_id not in pr["confirmer_ids"]:
                pr["confirmer_ids"].append(conf_id)
            pr["status"] = "active" if len(pr["confirmer_ids"]) >= 2 else "pending"
        return {"data": {"approvePassRequest": True}}

    # rejectPassRequest(pass_request_id) -> sets rejected permanently
    if "rejectPassRequest" in q:
        pr_id = vars_.get("pass_request_id") or vars_.get("id")
        pr = next((v for v in state["pass_requests_by_pass_id"].values() if v["id"] == pr_id), None)
        if pr:
            pr["status"] = "rejected"
        return {"data": {"rejectPassRequest": True}}

    # deleteUser/deletePlace/deletePass/deleteTicket etc -> True
    if "deleteTicket" in q:
        return {"data": {"deleteTicket": True}}
    if "deleteUser" in q or "deletePlace" in q or "deletePass" in q:
        return {"data": {"ok": True}}

    return {"data": {}}


def build_headers(access_token: str, *, company_id: str = DEFAULT_COMPANY_ID) -> dict[str, str]:
    return {
        "Authorization": f"Bearer {access_token}",
        "x-company-id": company_id,
    }


def execute_graphql(
    *,
    query: str,
    variables: Optional[dict[str, Any]] = None,
    graphql_url: Optional[str] = None,
    company_id: str = DEFAULT_COMPANY_ID,
    access_token: Optional[str] = None,
    timeout_s: int = 30,
) -> dict[str, Any]:
    graphql_url = graphql_url or os.getenv("GRAPHQL_URL") or DEFAULT_GRAPHQL_URL
    if not graphql_url:
        raise ValueError("Не задан GRAPHQL_URL (передайте graphql_url или установите env GRAPHQL_URL)")

    token = access_token or get_access_token()

    if os.getenv("SUBSCRIBE_BUNDLE_MOCKS") in {"1", "true", "True"}:
        from worklib.subscribe_bundle_graphql_mock import execute_subscribe_bundle_mock_graphql

        data = execute_subscribe_bundle_mock_graphql(
            query=query,
            variables=variables,
            company_id=company_id,
            access_token=token,
        )
        if isinstance(data, dict) and data.get("errors"):
            raise RuntimeError(f"GraphQL errors: {data['errors']}")
        return data

    if os.getenv("PASSREQUESTS_MOCKS") in {"1", "true", "True"}:
        data = _mock_passrequests_execute_graphql(query=query, variables=variables, company_id=company_id, access_token=token)
        if isinstance(data, dict) and data.get("errors"):
            raise RuntimeError(f"GraphQL errors: {data['errors']}")
        return data

    headers = build_headers(token, company_id=company_id)

    payload: dict[str, Any] = {"query": query}
    if variables is not None:
        payload["variables"] = variables

    req = urllib.request.Request(
        graphql_url,
        data=json.dumps(payload).encode("utf-8"),
        headers={"content-type": "application/json", **headers},
        method="POST",
    )
    try:
        ctx = _build_ssl_context(graphql_url)
        with urllib.request.urlopen(req, timeout=timeout_s, context=ctx) as resp:
            raw = resp.read().decode("utf-8")
    except urllib.error.HTTPError as e:
        body = e.read().decode("utf-8", errors="replace")
        raise RuntimeError(f"GraphQL HTTP {e.code}: {body}") from e

    data = json.loads(raw) if raw else {}
    if isinstance(data, dict) and data.get("errors"):
        errors = data["errors"]
        # На стенде часть операций возвращает Forbidden как GraphQL error (не HTTP 403).
        if isinstance(errors, list) and any(getattr(err, "get", lambda *_: None)("status") == 403 for err in errors):
            raise PermissionError(
                "Forbidden (403) для GraphQL операции. "
                "Проверьте креды/права. Можно задать env: AUTH_USERNAME/AUTH_PASSWORD/AUTH_GRANT_TYPE."
            )
        raise RuntimeError(f"GraphQL errors: {errors}")
    if not isinstance(data, dict):
        raise RuntimeError(f"GraphQL response is not an object: {data!r}")
    return data

