# pyright: reportCallIssue=false

from __future__ import annotations

import json
import os
import time
from dataclasses import dataclass, field
from typing import Any, Callable, Optional

import allure  # pyright: ignore[reportMissingImports]
from allure_commons.types import AttachmentType  # pyright: ignore[reportMissingImports]

from KVSTest.testdata.kvs_test_data import KVSTestData
from worklib.graphql_client import DEFAULT_COMPANY_ID, execute_graphql


def _attach_json(name: str, payload: Any) -> None:
    allure.attach(
        json.dumps(payload, ensure_ascii=False, indent=2),
        name=name,
        attachment_type=AttachmentType.JSON,
    )


def _exec_or_fail(*, op_name: str, token: str, query: str, variables: dict[str, Any] | None = None, company_id: str) -> dict[str, Any]:
    try:
        return execute_graphql(
            query=query,
            variables=variables,
            company_id=company_id,
            access_token=token,
        )
    except PermissionError as e:
        allure.attach(str(e), name=f"Forbidden: {op_name}", attachment_type=AttachmentType.TEXT)
        raise AssertionError(f"Forbidden на операции: {op_name}") from e


BUNDLE_SCOPE_QUERY = """
query bundleScope($place_id: String!, $pid: String!) {
  members(filters: { place_id: $place_id }) {
    results {
      id
      user { id }
    }
  }
  place(id: $pid) {
    results {
      id
      services { id title }
    }
  }
}
""".strip()

MEMBERS_ONLY_QUERY = """
query membersByPlace($place_id: String!) {
  members(filters: { place_id: $place_id }) {
    results {
      id
      user { id }
    }
  }
}
""".strip()


@dataclass
class SubscribeBundleTestData:
    """
    Два места, три сервиса, два тарифа (plan) — «пакет» из двух сервисов на место 1 и отдельный на место 2,
    подписка пользователя только на место 1. Проверка: members + сервисы места (place.services) для того же place_id.
    """

    company_id: str = DEFAULT_COMPANY_ID
    parent_place_id: str = "6915dc03462d5aea0adc8cbd"

    kvs: KVSTestData | None = None
    place_a_id: Optional[str] = None
    place_b_id: Optional[str] = None
    service_ids: list[str] = field(default_factory=list)
    plan_bundle_id: Optional[str] = None
    plan_place_b_id: Optional[str] = None
    subscriber_id: Optional[str] = None
    subscription_id: Optional[str] = None
    subscription_services_ids: list[str] = field(default_factory=list)
    _bundle_scope_uses_place_services: Optional[bool] = None

    _cleanup_fns: Optional[list[Callable[[], None]]] = None

    @classmethod
    def from_behave_context(cls, context: Any, *, company_id: str = DEFAULT_COMPANY_ID) -> "SubscribeBundleTestData":
        td: SubscribeBundleTestData | None = getattr(context, "subscribe_bundle_test_data", None)
        if isinstance(td, cls):
            if not td._cleanup_fns:
                td._cleanup_fns = getattr(context, "_cleanup_fns", None)
            if not td.kvs:
                td.kvs = KVSTestData.from_behave_context(context, company_id=company_id)
            return td
        td = cls(company_id=company_id)
        td._cleanup_fns = getattr(context, "_cleanup_fns", None)
        td.kvs = KVSTestData.from_behave_context(context, company_id=company_id)
        setattr(context, "subscribe_bundle_test_data", td)
        return td

    def _register_cleanup(self, fn: Callable[[], None]) -> None:
        if self._cleanup_fns is not None:
            self._cleanup_fns.append(fn)

    def ensure_token(self) -> str:
        assert self.kvs is not None
        return self.kvs.ensure_token()

    def prepare_two_places_three_services_plans_subscription(self) -> None:
        token = self.ensure_token()
        suffix = str(int(time.time()))

        mutation_places = """
mutation ($place_type: PlaceType!, $names: [String!]!, $parent_id: String) {
  createPlaceMultiple(
    dto: {place_type: $place_type, names: $names, parent_id: $parent_id}
  ) {
    id
    __typename
  }
}
""".strip()
        variables_places = {
            "names": [f"bundle-a-{suffix}", f"bundle-b-{suffix}"],
            "parent_id": self.parent_place_id,
            "place_type": "flat",
        }
        with allure.step("GraphQL: createPlaceMultiple (two places)"):
            resp = _exec_or_fail(
                op_name="createPlaceMultiple(mutation)",
                token=token,
                query=mutation_places,
                variables=variables_places,
                company_id=self.company_id,
            )
            _attach_json("createPlaceMultiple (bundle)", resp)
            created = resp.get("data", {}).get("createPlaceMultiple")
            rows = created if isinstance(created, list) else [created]
            ids = [r.get("id") for r in rows if isinstance(r, dict) and r.get("id")]
            assert len(ids) >= 2, f"Ожидали 2 места, получили: {rows!r}"
            self.place_a_id, self.place_b_id = ids[0], ids[1]

        service_cleanup_fns: list[Callable[[], None]] = []

        def _mk_service(title: str) -> str:
            m = """
mutation createservice($title: String!, $type: String!) {
  createService(dto: { title: $title, type: $type }) {
    id
    title
    type
  }
}
""".strip()
            r = _exec_or_fail(
                op_name="createService(mutation)",
                token=token,
                query=m,
                variables={"title": title, "type": "access"},
                company_id=self.company_id,
            )
            svc = r.get("data", {}).get("createService")
            assert isinstance(svc, dict) and svc.get("id"), f"createService: {r!r}"
            sid = str(svc["id"])

            def _del_svc(s: str = sid) -> None:
                dm = """mutation { deleteService(id: "%s") }""".strip() % s
                _exec_or_fail(op_name="deleteService(mutation)", token=self.ensure_token(), query=dm, variables=None, company_id=self.company_id)

            service_cleanup_fns.append(_del_svc)
            return sid

        with allure.step("GraphQL: createService x3"):
            s1 = _mk_service(f"bundle-s1-{suffix}")
            s2 = _mk_service(f"bundle-s2-{suffix}")
            s3 = _mk_service(f"bundle-s3-{suffix}")
            self.service_ids = [s1, s2, s3]

        bind_m = """
mutation ($dto: AddPlaceToServiceInput!) {
  addPlaceToService(dto: $dto) { id }
}
""".strip()

        def _bind(sid: str, pid: str) -> None:
            _exec_or_fail(
                op_name="addPlaceToService",
                token=token,
                query=bind_m,
                variables={"dto": {"service_id": sid, "place_id": pid}},
                company_id=self.company_id,
            )

        with allure.step("GraphQL: addPlaceToService (s1,s2 -> place A; s3 -> place B)"):
            assert self.place_a_id and self.place_b_id
            _bind(s1, self.place_a_id)
            _bind(s2, self.place_a_id)
            _bind(s3, self.place_b_id)

        def _unbind_all() -> None:
            tok = self.ensure_token()
            um = """
mutation ($dto: AddPlaceToServiceInput!) {
  removePlaceFromService(dto: $dto) { id }
}
""".strip()
            for sid, pid in ((s1, self.place_a_id), (s2, self.place_a_id), (s3, self.place_b_id)):
                _exec_or_fail(op_name="removePlaceFromService", token=tok, query=um, variables={"dto": {"service_id": sid, "place_id": pid}}, company_id=self.company_id)

        plan_m = """
mutation ($input: CreatePlanDTO!) {
  createPlan(dto: $input) {
    id
    service_ids
    place_ids
    title
  }
}
""".strip()
        with allure.step("GraphQL: createPlan (bundle: two services on place A)"):
            r1 = _exec_or_fail(
                op_name="createPlan(bundle)",
                token=token,
                query=plan_m,
                variables={
                    "input": {
                        "services": [s1, s2],
                        "place_ids": [self.place_a_id],
                        "price": 100,
                        "payment_interval": 1,
                        "discount": 0,
                        "title": f"bundle-plan-{suffix}",
                    }
                },
                company_id=self.company_id,
            )
            _attach_json("createPlan bundle", r1)
            p1 = r1.get("data", {}).get("createPlan")
            assert isinstance(p1, dict) and p1.get("id"), f"createPlan: {r1!r}"
            self.plan_bundle_id = str(p1["id"])

        with allure.step("GraphQL: createPlan (single service on place B)"):
            r2 = _exec_or_fail(
                op_name="createPlan(placeB)",
                token=token,
                query=plan_m,
                variables={
                    "input": {
                        "services": [s3],
                        "place_ids": [self.place_b_id],
                        "price": 50,
                        "payment_interval": 1,
                        "discount": 0,
                        "title": f"tariff-b-{suffix}",
                    }
                },
                company_id=self.company_id,
            )
            _attach_json("createPlan place B", r2)
            p2 = r2.get("data", {}).get("createPlan")
            assert isinstance(p2, dict) and p2.get("id"), f"createPlan B: {r2!r}"
            self.plan_place_b_id = str(p2["id"])

        def _del_plan_b() -> None:
            if self.plan_place_b_id:
                dm = """mutation { deletePlan(id: "%s") }""".strip() % self.plan_place_b_id
                _exec_or_fail(op_name="deletePlan", token=self.ensure_token(), query=dm, variables=None, company_id=self.company_id)

        def _del_plan_bundle() -> None:
            if self.plan_bundle_id:
                dm = """mutation { deletePlan(id: "%s") }""".strip() % self.plan_bundle_id
                _exec_or_fail(op_name="deletePlan", token=self.ensure_token(), query=dm, variables=None, company_id=self.company_id)

        assert self.kvs is not None
        self.subscriber_id, _uname = self.kvs.create_new_user()
        add_mut = """
mutation AddUserToPlace($input: AddUserToPlaceDTO!) {
  addUserToPlace(dto: $input)
}
""".strip()
        with allure.step("GraphQL: addUserToPlace (subscriber -> place A only)"):
            ar = _exec_or_fail(
                op_name="addUserToPlace",
                token=token,
                query=add_mut,
                variables={"input": {"place_id": self.place_a_id, "account_id": self.subscriber_id}},
                company_id=self.company_id,
            )
            _attach_json("addUserToPlace bundle", ar)

        sub_m = """
mutation createsub($plan_id: String!, $subscriber_id: String!, $place_id: String!, $service_id: String!) {
  createSubscription(dto: { plan_id: $plan_id, subscriber_id: $subscriber_id, place_id: $place_id, service_id: $service_id }) {
    id
    services { id title }
    place_id
    user { id }
  }
}
""".strip()
        with allure.step("GraphQL: createSubscription (bundle plan, place A)"):
            sr = _exec_or_fail(
                op_name="createSubscription",
                token=token,
                query=sub_m,
                variables={
                    "plan_id": self.plan_bundle_id,
                    "subscriber_id": self.subscriber_id,
                    "place_id": self.place_a_id,
                    "service_id": s1,
                },
                company_id=self.company_id,
            )
            _attach_json("createSubscription bundle", sr)
            sub = sr.get("data", {}).get("createSubscription")
            assert isinstance(sub, dict) and sub.get("id"), f"createSubscription: {sr!r}"
            self.subscription_id = str(sub["id"])
            svcs = sub.get("services")
            if isinstance(svcs, list):
                self.subscription_services_ids = [str(x["id"]) for x in svcs if isinstance(x, dict) and x.get("id")]

        def _del_sub() -> None:
            if not self.subscription_id:
                return
            dm = """mutation($id: String!) { deleteSubscription(id: $id) }""".strip()
            _exec_or_fail(
                op_name="deleteSubscription",
                token=self.ensure_token(),
                query=dm,
                variables={"id": self.subscription_id},
                company_id=self.company_id,
            )

        pa, pb = self.place_a_id, self.place_b_id

        def _del_place_b_fn() -> None:
            if pb:
                dm = """mutation { deletePlace(id: "%s") }""".strip() % pb
                try:
                    _exec_or_fail(op_name="deletePlace", token=self.ensure_token(), query=dm, variables=None, company_id=self.company_id)
                except Exception:
                    pass

        def _del_place_a_fn() -> None:
            if pa:
                dm = """mutation { deletePlace(id: "%s") }""".strip() % pa
                try:
                    _exec_or_fail(op_name="deletePlace", token=self.ensure_token(), query=dm, variables=None, company_id=self.company_id)
                except Exception:
                    pass

        self._register_cleanup(_del_place_b_fn)
        self._register_cleanup(_del_place_a_fn)
        for fn in service_cleanup_fns:
            self._register_cleanup(fn)
        self._register_cleanup(_unbind_all)
        self._register_cleanup(_del_plan_b)
        self._register_cleanup(_del_plan_bundle)
        self._register_cleanup(_del_sub)

    def query_members_for_place(self, *, place_id: str) -> dict[str, Any]:
        token = self.ensure_token()
        with allure.step("GraphQL: members(filters.place_id)"):
            resp = _exec_or_fail(
                op_name="members(query)",
                token=token,
                query=MEMBERS_ONLY_QUERY,
                variables={"place_id": place_id},
                company_id=self.company_id,
            )
            _attach_json("members response", resp)
            return resp

    def query_bundle_scope(self, *, place_id: str) -> dict[str, Any]:
        token = self.ensure_token()
        if self._bundle_scope_uses_place_services is False:
            mresp = self.query_members_for_place(place_id=place_id)
            return {"data": {"members": mresp.get("data", {}).get("members"), "place": {"results": []}}}

        with allure.step("GraphQL: bundleScope (members + place.services)"):
            try:
                resp = execute_graphql(
                    query=BUNDLE_SCOPE_QUERY,
                    variables={"place_id": place_id, "pid": place_id},
                    company_id=self.company_id,
                    access_token=token,
                )
                self._bundle_scope_uses_place_services = True
            except RuntimeError as e:
                if "services" in str(e) and "PlaceObject" in str(e):
                    self._bundle_scope_uses_place_services = False
                    allure.attach(
                        str(e),
                        name="bundleScope: place.services not supported, using members + subscription.services",
                        attachment_type=AttachmentType.TEXT,
                    )
                    mresp = self.query_members_for_place(place_id=place_id)
                    return {
                        "data": {
                            "members": mresp.get("data", {}).get("members"),
                            "place": {"results": []},
                        }
                    }
                raise
            _attach_json("bundleScope response", resp)
            return resp

    def assert_user_sees_only_place_services_via_members_and_place(self) -> None:
        assert self.place_a_id and self.place_b_id and self.subscriber_id
        assert len(self.service_ids) >= 3
        s1, s2, s3 = self.service_ids[0], self.service_ids[1], self.service_ids[2]

        for label, pid in ("placeA", self.place_a_id), ("placeB", self.place_b_id):
            resp = self.query_bundle_scope(place_id=pid)
            members = resp.get("data", {}).get("members", {}).get("results", [])
            assert isinstance(members, list), f"{label}: members.results не list"
            user_ids = [m.get("user", {}).get("id") for m in members if isinstance(m, dict) and isinstance(m.get("user"), dict)]
            if label == "placeA":
                assert self.subscriber_id in user_ids, f"Подписчик должен быть member места A: {user_ids!r}"
            else:
                assert self.subscriber_id not in user_ids, f"Подписчик не должен быть member места B: {user_ids!r}"

            places = resp.get("data", {}).get("place", {}).get("results", [])
            services = None
            if isinstance(places, list) and places and isinstance(places[0], dict):
                services = places[0].get("services")

            if isinstance(services, list) and services:
                svc_ids = {s.get("id") for s in services if isinstance(s, dict)}
                if label == "placeA":
                    assert s1 in svc_ids and s2 in svc_ids, f"Место A должно содержать s1,s2: {svc_ids!r}"
                    assert s3 not in svc_ids, f"Сервис места B не должен быть на месте A: {svc_ids!r}"
                else:
                    assert s3 in svc_ids, f"Место B должно содержать s3: {svc_ids!r}"
                    assert s1 not in svc_ids and s2 not in svc_ids, f"Сервисы места A не должны быть на B: {svc_ids!r}"
            elif label == "placeA" and self.subscription_services_ids:
                sub_ids = {str(x) for x in self.subscription_services_ids}
                assert sub_ids <= {s1, s2}, f"Подписка на месте A должна включать только сервисы пакета A: {sub_ids!r}, ожидали подмножество {{{s1!r}, {s2!r}}}"
                assert s3 not in sub_ids, f"Подписка не должна содержать сервис места B (s3): {sub_ids!r}"
            elif label == "placeA":
                allure.attach(
                    "Нет place.services и createSubscription не вернул services — ослабленная проверка только по members.",
                    name="bundleScope: limited assertions",
                    attachment_type=AttachmentType.TEXT,
                )
