from __future__ import annotations

import json
import re
from pathlib import Path
from typing import TYPE_CHECKING, Any, Optional
from urllib.parse import urlparse, parse_qs, unquote

import requests.models

from mockmesh.core.fallback import FallbackConfig, safe_intercept
from mockmesh.core.logger import get_logger
from mockmesh.interceptors.common import (
    parse_request_body,
    decode_json,
    encode_response,
    cloud_provider_for_url,
)
from mockmesh.interceptors.gcp.storage       import handle_gcs, handle_gcs_bucket, _RAW_KEY
from mockmesh.interceptors.gcp.pubsub        import handle_pubsub
from mockmesh.interceptors.gcp.secretmanager import handle_secret_manager
from mockmesh.interceptors.gcp.firestore     import handle_firestore
from mockmesh.interceptors.loader            import load_json
from mockmesh.plugins.base                   import BasePlugin

if TYPE_CHECKING:
    from mockmesh.response.engine import ResponseEngine
    from mockmesh.storage.manager import StorageManager

_log = get_logger("interceptors.gcp")

_DEFAULTS_JSON = Path(__file__).parent.parent.parent / "defaults" / "gcp.json"


def _is_gcp_url(url: str) -> bool:
    return cloud_provider_for_url(url) == "gcp"


def _is_auth_url(url: str, defaults: dict) -> bool:
    auth_hosts = defaults.get("_config", {}).get("auth", {}).get("hosts", [])
    return any(h in url for h in auth_hosts)


def _is_auth_url_static(url: str) -> bool:
    _AUTH_MARKERS = ("oauth2.googleapis.com", "accounts.google.com",
                     "metadata.google.internal", "iamcredentials.googleapis.com")
    return any(m in url for m in _AUTH_MARKERS)


def _is_metadata_url(url: str) -> bool:
    return "metadata.google.internal" in url


def _mock_auth_response(url: str, defaults: dict) -> dict:
    auth = defaults.get("_config", {}).get("auth", {})
    if _is_metadata_url(url):
        token_paths = auth.get("token_paths", [])
        if any(p in url for p in token_paths):
            return auth.get("token", {})
        if "project" in url:
            return auth.get("project_info", {})
        return auth.get("instance_info", {})
    return auth.get("token", {})


def _parse_gcp_url(url: str) -> dict:
    out: dict = {
        "svc": "other", "project": "mock-project",
        "resource": "", "name": "", "sub_op": "",
    }

    if _is_auth_url_static(url) or _is_metadata_url(url):
        out["svc"] = "auth"
        return out

    parsed_url = urlparse(url)
    qs         = parse_qs(parsed_url.query)
    path       = parsed_url.path

    if "storage.googleapis.com" in url:
        out["svc"]      = "storage"
        out["resource"] = "objects"

        m_obj = re.search(r"/b/([^/?]+)/o(?:/([^?]*))?", path)
        if m_obj:
            out["name"]    = m_obj.group(1) or ""
            raw_obj        = m_obj.group(2) or ""
            if not raw_obj:
                raw_obj = (qs.get("name") or [""])[0]
            out["sub_op"]  = unquote(raw_obj)
        else:
            m_b = re.search(r"/b/([^/?]+)", path)
            if m_b:
                out["name"]     = m_b.group(1) or ""
                out["resource"] = "buckets"
            else:
                out["resource"] = "buckets"
        return out

    if "pubsub.googleapis.com" in url:
        out["svc"] = "pubsub"
        m = re.search(r"/projects/([^/]+)/(topics|subscriptions)/([^/:?]*)", path)
        if m:
            out["project"]  = m.group(1)
            out["resource"] = m.group(2)
            out["name"]     = m.group(3)
        if ":" in path:
            out["sub_op"] = path.rsplit(":", 1)[-1].split("?")[0]
        elif not m:
            m2 = re.search(r"/projects/([^/]+)/(topics|subscriptions)$", path)
            if m2:
                out["project"]  = m2.group(1)
                out["resource"] = m2.group(2)
                out["sub_op"]   = "list"
        return out

    if "secretmanager.googleapis.com" in url:
        out["svc"] = "secretmanager"
        m = re.search(r"/projects/([^/]+)/secrets/([^/:?]*)", path)
        if m:
            out["project"] = m.group(1)
            out["name"]    = m.group(2)
        if ":" in path:
            out["sub_op"] = path.rsplit(":", 1)[-1].split("?")[0]
        elif "/versions/" in path:
            m2 = re.search(r"/versions/([^/:?]*)", path)
            if m2:
                out["sub_op"] = m2.group(1)
        return out

    if "firestore.googleapis.com" in url:
        out["svc"] = "firestore"
        m = re.search(
            r"/projects/([^/]+)/databases/([^/]+)/documents/([^/:?]+)(?:/([^?:]*))?" , path
        )
        if m:
            out["project"]  = m.group(1)
            out["resource"] = m.group(2)
            out["name"]     = m.group(3)
            out["sub_op"]   = m.group(4) or ""
        if ":" in path:
            out["sub_op"] = path.rsplit(":", 1)[-1].split("?")[0]
        return out

    if "container.googleapis.com" in url:
        out["svc"] = "gke"
        m = re.search(r"/projects/([^/]+)", path)
        if m:
            out["project"] = m.group(1)
        return out

    if "run.googleapis.com" in url or "run.cloud.google.com" in url:
        out["svc"] = "cloudrun"
        m = re.search(r"/projects/([^/]+)", path)
        if m:
            out["project"] = m.group(1)
        return out

    if "bigquery.googleapis.com" in url:
        out["svc"] = "bigquery"
        m = re.search(r"/projects/([^/]+)", path)
        if m:
            out["project"] = m.group(1)
        return out

    return out


def _default_gcp_body(parsed: dict, method: str, defaults: dict) -> dict:
    svc     = parsed["svc"]
    svc_map = defaults.get(svc, {})
    resource = parsed.get("resource", "")
    name     = parsed.get("name", "")
    sub_op   = parsed.get("sub_op", "")

    for key in (
        f"{resource}_{method.lower()}",
        f"{method.lower()}",
        "get",
    ):
        if key in svc_map:
            resp = dict(svc_map[key])
            if name and "name" in resp:
                resp["name"] = name
            return resp

    return {"mocked": True, "service": svc}


class FakeGCPResponse:
    def __init__(self, status: int, headers: dict, body: bytes) -> None:
        self.status_code = status
        self.headers     = {"Content-Type": "application/json", **headers}
        self._body       = body
        self.content     = body
        self.encoding    = "utf-8"

    @property
    def text(self) -> str:
        return self._body.decode("utf-8", errors="replace")

    def json(self) -> Any:
        return json.loads(self._body or b"{}")

    def raise_for_status(self) -> None:
        if self.status_code >= 400:
            raise Exception(f"GCP mock HTTP {self.status_code}")


class GcpInterceptor(BasePlugin):

    name = "gcp"

    def __init__(self, response_engine: "ResponseEngine") -> None:
        super().__init__(response_engine)
        self._defaults: dict = load_json(_DEFAULTS_JSON, "map")

    def activate(
        self,
        config: dict,
        *,
        storage: Optional["StorageManager"] = None,
        provider_defaults: Optional[dict] = None,
        fallback_config: Optional[FallbackConfig] = None,
        custom_handlers: Optional[dict] = None,
        **_: Any,
    ) -> None:
        self.storage = storage
        self._fallback_config = fallback_config
        self._custom_handlers = custom_handlers if custom_handlers is not None else {}
        if provider_defaults is not None:
            self._defaults = provider_defaults.get("gcp", self._defaults)

        from mockmesh.interceptors.http import HttpInterceptor
        interceptor = self

        def _gcp_dispatch(adapter_self: Any, request: Any, **kwargs: Any) -> Any:
            url: str = getattr(request, "url", "") or ""
            method: str = (getattr(request, "method", "GET") or "GET").upper()
            return safe_intercept(
                provider="gcp",
                operation=f"{method} {url[:80]}",
                handler_fn=interceptor._handle_request,
                handler_args=(request, url, method),
                fallback_config=interceptor._fallback_config,
                generic_response=_make_response(200, {"Content-Type": "application/json"},
                                                encode_response({"mocked": True, "service": "gcp"})),
                custom_handlers=interceptor._custom_handlers,
            )

        HttpInterceptor.register_url_dispatcher("gcp", _is_gcp_url, _gcp_dispatch)
        self._active = True
        _log.info("GCP interceptor activated",
                  extra={"mm_patched": "HttpInterceptor URL dispatch (gcp)"})

    def deactivate(self) -> None:
        if not self._active:
            return
        from mockmesh.interceptors.http import HttpInterceptor
        HttpInterceptor.unregister_url_dispatcher("gcp")
        self._active = False
        _log.info("GCP interceptor deactivated")

    def _handle_request(self, request: Any, url: str, method: str) -> Any:
        if _is_auth_url(url, self._defaults) or _is_metadata_url(url):
            _log.info("GCP intercepted (auth/metadata)", extra={"mm_url": url[:80]})
            body_bytes = encode_response(_mock_auth_response(url, self._defaults))
            return _make_response(200, {"Content-Type": "application/json"}, body_bytes)

        raw_body  = parse_request_body(request)
        body_data = decode_json(raw_body)
        parsed    = _parse_gcp_url(url)

        result: Optional[Any] = None

        if self.storage and self.storage._ready:
            result = self._dispatch_storage(parsed, method, raw_body, body_data, url)

        if result is None:
            result = _default_gcp_body(parsed, method, self._defaults)

        if isinstance(result, dict) and _RAW_KEY in result:
            raw_bytes  = result[_RAW_KEY]
            ctype      = result.get("_content_type", "application/octet-stream")
            _log.info("GCP intercepted (raw media)",
                      extra={"mm_svc": parsed["svc"], "mm_url": url[:80]})
            return _make_response(200, {"Content-Type": ctype}, raw_bytes)

        if isinstance(result, dict) and not result:
            status = 204 if method == "DELETE" else 200
        else:
            status = 200

        _log.info("GCP intercepted",
                  extra={"mm_svc": parsed["svc"], "mm_method": method, "mm_url": url[:80]})

        body_bytes = encode_response(result)
        return _make_response(status, {"Content-Type": "application/json"}, body_bytes)

    def _dispatch_storage(
        self,
        parsed: dict,
        method: str,
        raw_body: bytes,
        body_data: dict,
        url: str,
    ) -> Optional[Any]:
        sm  = self.storage
        svc = parsed["svc"]
        tpl = self._defaults.get(svc, {})

        if svc == "storage":
            bucket      = parsed["name"]
            object_name = parsed["sub_op"]
            if not bucket:
                return None
            if parsed["resource"] == "objects":
                return handle_gcs(sm, bucket, object_name, method, raw_body, tpl, url)
            return handle_gcs_bucket(sm, bucket, method, tpl)

        if svc == "pubsub":
            resource = parsed["resource"]
            name     = parsed["name"]
            sub_op   = parsed["sub_op"]
            op_map = {"PUT": "create", "DELETE": "delete", "GET": "get"}
            if sub_op in ("publish", "pull", "acknowledge", "modifyAckDeadline", "list"):
                operation = sub_op
            elif not name and method == "GET":
                operation = "list"
            else:
                operation = op_map.get(method, "create" if method == "POST" else "get")
            body_data["_name"] = name
            return handle_pubsub(sm, parsed["project"], resource, operation, body_data, tpl)

        if svc == "secretmanager":
            secret_id = parsed["name"]
            sub_op    = parsed["sub_op"]
            if not secret_id:
                qs        = parse_qs(urlparse(url).query)
                secret_id = (qs.get("secretId") or [""])[0]
            if sub_op in ("addVersion", "access", "listVersions",
                          "disableVersion", "destroyVersion"):
                operation = sub_op
            elif sub_op and sub_op.isdigit():
                operation = "getVersion"
            elif not secret_id and method == "GET":
                operation = "list"
            elif not secret_id:
                operation = "create"
            else:
                op_map = {"GET": "get", "DELETE": "delete",
                          "PATCH": "addVersion", "POST": "create"}
                operation = op_map.get(method, "get")
            return handle_secret_manager(
                sm, parsed["project"], secret_id or "default-secret",
                operation, body_data, tpl
            )

        if svc == "firestore":
            database   = parsed["resource"] or "(default)"
            collection = parsed["name"]
            doc_id     = parsed["sub_op"]
            sub_op     = parsed["sub_op"]
            if sub_op in ("runQuery",):
                operation = "runQuery"
            elif sub_op == "batchWrite" or "batchWrite" in url:
                operation = "batchWrite"
            else:
                op_map = {
                    "GET":    "getDocument"    if doc_id else "listDocuments",
                    "POST":   "createDocument",
                    "PATCH":  "updateDocument",
                    "DELETE": "deleteDocument",
                }
                operation = op_map.get(method, "getDocument")
            return handle_firestore(
                sm, parsed["project"], database, collection, doc_id,
                operation, body_data, tpl
            )

        return None


def _make_response(status: int, headers: dict, body: bytes) -> Any:
    resp             = requests.models.Response()
    resp.status_code = status
    resp.headers.update(headers)
    resp._content    = body
    resp.encoding    = "utf-8"
    return resp
