from __future__ import annotations

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

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

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


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
    except RuntimeError as e:
        # На части стендов Forbidden иногда приходит как GraphQL error со статусом 500.
        if "Forbidden" in str(e):
            allure.attach(str(e), name=f"Forbidden (runtime): {op_name}", attachment_type=AttachmentType.TEXT)
            raise AssertionError(
                f"Forbidden на операции: {op_name}. Проверьте, что пользователь из env "
                "AUTH_USERNAME/AUTH_PASSWORD имеет права на subscription операции."
            ) from e
        raise


@dataclass
class SubscriptionTestData:
    """
    Данные для теста подписок:
    service -> plan -> subscription -> invoices -> delete subscription
    """

    company_id: str = DEFAULT_COMPANY_ID
    kvs: KVSTestData | None = None

    service_id: Optional[str] = None
    plan_id: Optional[str] = None
    subscription_id: Optional[str] = None

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

    @classmethod
    def from_behave_context(cls, context: Any, *, company_id: str = DEFAULT_COMPANY_ID) -> "SubscriptionTestData":
        td: SubscriptionTestData | None = getattr(context, "kvs_subscription_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, "kvs_subscription_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_service(self) -> str:
        if self.service_id:
            return self.service_id
        assert self.kvs is not None
        token = self.kvs.ensure_token()
        mutation = """
mutation createservice($title: String!, $type: String!) {
  createService(dto: { title: $title, type: $type }) {
    id
    title
    type
  }
}
""".strip()
        suffix = str(int(time.time()))
        # API валидирует type: camera|intercom|access
        variables = {"title": f"kvs-service-{suffix}", "type": "access"}
        with allure.step("GraphQL: createService"):
            resp = _exec_or_fail(op_name="createService(mutation)", token=token, query=mutation, variables=variables, company_id=self.company_id)
            _attach_json("createService response", resp)
            service = resp.get("data", {}).get("createService")
            assert isinstance(service, dict) and service.get("id"), f"createService не вернул id. Ответ: {resp}"
            service_id = service["id"]
            self.service_id = service_id

            def _cleanup_delete_service() -> None:
                delete_mutation = """mutation { deleteService(id: "%s") }""".strip() % service_id
                _exec_or_fail(op_name="deleteService(mutation)", token=token, query=delete_mutation, variables=None, company_id=self.company_id)

            self._register_cleanup(_cleanup_delete_service)
            return service_id

    def ensure_plan(self) -> str:
        if self.plan_id:
            return self.plan_id
        assert self.kvs is not None
        token = self.kvs.ensure_token()
        service_id = self.ensure_service()
        place_id = self.kvs.ensure_place()

        mutation = """
mutation ($input: CreatePlanDTO!) {
  createPlan(dto: $input) {
    id
    service_ids
    bundle_ids
    place_id
    place_ids
    price
    title
    discount
    payment_interval
    price_without_discount
  }
}
""".strip()
        suffix = str(int(time.time()))
        variables = {
            "input": {
                "services": [service_id],
                "place_ids": [place_id],
                "price": 200,
                "payment_interval": 1,
                "discount": 0,
                "title": f"plan-kvs-{suffix}",
            }
        }
        with allure.step("GraphQL: createPlan"):
            resp = _exec_or_fail(op_name="createPlan(mutation)", token=token, query=mutation, variables=variables, company_id=self.company_id)
            _attach_json("createPlan response", resp)
            plan = resp.get("data", {}).get("createPlan")
            assert isinstance(plan, dict) and plan.get("id"), f"createPlan не вернул id. Ответ: {resp}"
            # Проверяем, что service действительно включён в plan (как просили)
            service_ids = plan.get("service_ids")
            assert isinstance(service_ids, list) and service_id in service_ids, (
                f"Созданный service_id не попал в plan.service_ids. service_id={service_id!r}, service_ids={service_ids!r}"
            )
            plan_id = plan["id"]
            self.plan_id = plan_id

            def _cleanup_delete_plan() -> None:
                delete_mutation = """mutation { deletePlan(id: "%s") }""".strip() % plan_id
                _exec_or_fail(op_name="deletePlan(mutation)", token=token, query=delete_mutation, variables=None, company_id=self.company_id)

            self._register_cleanup(_cleanup_delete_plan)
            return plan_id

    def create_subscription(self) -> str:
        if self.subscription_id:
            return self.subscription_id
        assert self.kvs is not None
        token = self.kvs.ensure_token()
        plan_id = self.ensure_plan()
        service_id = self.ensure_service()
        place_id = self.kvs.ensure_place()
        subscriber_id = self.kvs.create_user()

        # Пользователь должен быть участником места (как в addUserToPlace тесте)
        add_resp = self.kvs.add_user_to_place(account_id=subscriber_id, place_id=place_id)
        _attach_json("addUserToPlace (for subscription) response", add_resp)
        payload = add_resp.get("data", {}).get("addUserToPlace")
        assert isinstance(payload, dict) and payload.get("member_id"), f"addUserToPlace не вернул member_id: {add_resp!r}"

        members_resp = self.kvs.query_place_members(place_id=place_id)
        _attach_json("place members (after addUserToPlace) response", members_resp)
        results = members_resp.get("data", {}).get("place", {}).get("results", [])
        assert isinstance(results, list) and results, f"place.results пустой: {members_resp!r}"
        members = results[0].get("members") if isinstance(results[0], dict) else None
        assert isinstance(members, list), f"members не list: {members!r}"
        user_ids = []
        for m in members:
            if isinstance(m, dict) and isinstance(m.get("user"), dict):
                uid = m["user"].get("id")
                if isinstance(uid, str):
                    user_ids.append(uid)
        assert subscriber_id in user_ids, f"subscriber_id не найден в place.members.user.id. subscriber_id={subscriber_id!r}, got={user_ids!r}"

        mutation = """
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 }
    user { id data { first_name last_name } }
    plan { id title }
    place_id
  }
}
""".strip()
        variables = {"plan_id": plan_id, "subscriber_id": subscriber_id, "place_id": place_id, "service_id": service_id}
        with allure.step("GraphQL: createSubscription"):
            resp = _exec_or_fail(op_name="createSubscription(mutation)", token=token, query=mutation, variables=variables, company_id=self.company_id)
            _attach_json("createSubscription response", resp)
            sub = resp.get("data", {}).get("createSubscription")
            assert isinstance(sub, dict) and sub.get("id"), f"createSubscription не вернул id. Ответ: {resp}"
            # Проверяем, что service в subscription совпадает с созданным
            services = sub.get("services")
            assert isinstance(services, list) and any(isinstance(s, dict) and s.get("id") == service_id for s in services), (
                f"service_id не найден в subscription.services. service_id={service_id!r}, services={services!r}"
            )
            plan_obj = sub.get("plan")
            assert isinstance(plan_obj, dict) and plan_obj.get("id") == plan_id, f"subscription.plan.id не совпал: {plan_obj!r}"
            assert sub.get("place_id") == place_id, f"subscription.place_id не совпал: {sub!r}"
            user_obj = sub.get("user")
            assert isinstance(user_obj, dict) and user_obj.get("id") == subscriber_id, f"subscription.user.id не совпал: {user_obj!r}"
            subscription_id = sub["id"]
            self.subscription_id = subscription_id

            def _cleanup_delete_subscription() -> None:
                del_mut = """mutation($id: String!) { deleteSubscription(id: $id) }""".strip()
                _exec_or_fail(op_name="deleteSubscription(mutation)", token=token, query=del_mut, variables={"id": subscription_id}, company_id=self.company_id)

            self._register_cleanup(_cleanup_delete_subscription)
            return subscription_id

    def query_invoices(self) -> dict[str, Any]:
        assert self.kvs is not None
        token = self.kvs.ensure_token()
        place_id = self.kvs.ensure_place()
        query = """
query invoicesInfo($place_id: String!) {
  invoices(place_id: $place_id, status: pending) {
    id
    price
    status
    subscriptions
    place_id
  }
}
""".strip()
        with allure.step("GraphQL: invoices (pending)"):
            resp = _exec_or_fail(op_name="invoices(query)", token=token, query=query, variables={"place_id": place_id}, company_id=self.company_id)
            _attach_json("invoices response", resp)
            return resp

    def delete_subscription_now(self) -> dict[str, Any]:
        assert self.kvs is not None
        token = self.kvs.ensure_token()
        sub_id = self.subscription_id
        assert isinstance(sub_id, str) and sub_id, "Нет subscription_id."
        mutation = """mutation($id: String!) { deleteSubscription(id: $id) }""".strip()
        with allure.step("GraphQL: deleteSubscription"):
            resp = _exec_or_fail(op_name="deleteSubscription(mutation)", token=token, query=mutation, variables={"id": sub_id}, company_id=self.company_id)
            _attach_json("deleteSubscription response", resp)
            return resp

