#!/usr/bin/env python3

import argparse
import http.cookiejar
import json
import sys
import urllib.error
import urllib.parse
import urllib.request


DESIRED_FLAGS = {
    "syntheticMonitoringCardDismissed": "true",
    "irmCardDismissed": "true",
    "enterpriseAuthCardDismissed": "true",
    "gettingStartedPanelDismissed": "true",
}


class GrafanaClient:
    def __init__(self, base_url: str, cookie_header: str | None):
        self.base_url = base_url.rstrip("/")
        self.cookie_header = cookie_header
        self.cookie_jar = http.cookiejar.CookieJar()
        self.opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(self.cookie_jar))

    def request(
        self,
        method: str,
        path: str,
        *,
        body: dict | None = None,
        headers: dict[str, str] | None = None,
        expected: tuple[int, ...] = (200,),
    ) -> tuple[int, dict | list | str | None]:
        all_headers = {"Accept": "application/json"}
        if headers:
            all_headers.update(headers)
        if self.cookie_header:
            all_headers["Cookie"] = self.cookie_header

        data = None
        if body is not None:
            data = json.dumps(body).encode("utf-8")
            all_headers.setdefault("Content-Type", "application/json")

        req = urllib.request.Request(
            self.base_url + path,
            data=data,
            headers=all_headers,
            method=method,
        )

        try:
            with self.opener.open(req) as resp:
                raw = resp.read()
                status = resp.getcode()
                parsed = self._parse_body(raw, resp.headers.get_content_type())
        except urllib.error.HTTPError as err:
            raw = err.read()
            status = err.code
            parsed = self._parse_body(raw, err.headers.get_content_type())
        except urllib.error.URLError as err:
            raise RuntimeError(f"request failed for {method} {path}: {err.reason}") from err

        if status not in expected:
            raise RuntimeError(
                f"{method} {path} returned {status}, expected {expected}. Response: {self._compact(parsed)}"
            )

        return status, parsed

    @staticmethod
    def _parse_body(raw: bytes, content_type: str | None):
        if not raw:
            return None
        text = raw.decode("utf-8", errors="replace")
        if content_type == "application/json":
            try:
                return json.loads(text)
            except json.JSONDecodeError:
                return text
        try:
            return json.loads(text)
        except json.JSONDecodeError:
            return text

    @staticmethod
    def _compact(value) -> str:
        if isinstance(value, (dict, list)):
            return json.dumps(value, sort_keys=True)
        return str(value)


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Persist Grafana OSS promo dismissals in backend user storage."
    )
    parser.add_argument(
        "--url",
        default="http://127.0.0.1:3000",
        help="Grafana base URL. Default: %(default)s",
    )
    parser.add_argument("--username", help="Grafana username for /login")
    parser.add_argument("--password", help="Grafana password for /login")
    parser.add_argument(
        "--cookie-header",
        help="Existing authenticated Cookie header, for example 'grafana_session=...'",
    )
    parser.add_argument(
        "--namespace",
        help="Override Grafana namespace. If omitted, the script tries /api/frontend/settings and then defaults to 'default'.",
    )
    return parser.parse_args()


def login_if_requested(client: GrafanaClient, args: argparse.Namespace) -> None:
    if args.cookie_header:
        return
    if bool(args.username) != bool(args.password):
        raise RuntimeError("pass both --username and --password, or neither")
    if not args.username:
        raise RuntimeError("authentication required: use --username/--password or --cookie-header")

    client.request(
        "POST",
        "/login",
        body={"user": args.username, "password": args.password},
        expected=(200,),
    )


def get_current_user(client: GrafanaClient) -> dict:
    _, payload = client.request("GET", "/api/user", expected=(200,))
    if not isinstance(payload, dict):
        raise RuntimeError(f"/api/user returned unexpected payload: {payload!r}")
    return payload


def discover_namespace(client: GrafanaClient, override: str | None) -> str:
    if override:
        return override

    try:
        _, payload = client.request("GET", "/api/frontend/settings", expected=(200,))
    except RuntimeError:
        return "default"

    if isinstance(payload, dict):
        for key in ("namespace",):
            value = payload.get(key)
            if isinstance(value, str) and value:
                return value

        boot_data = payload.get("bootData")
        if isinstance(boot_data, dict):
            for key in ("namespace",):
                value = boot_data.get(key)
                if isinstance(value, str) and value:
                    return value

            settings = boot_data.get("settings")
            if isinstance(settings, dict):
                value = settings.get("namespace")
                if isinstance(value, str) and value:
                    return value

    return "default"


def get_resource(client: GrafanaClient, namespace: str, resource_name: str) -> dict | None:
    path = f"/apis/userstorage.grafana.app/v0alpha1/namespaces/{namespace}/user-storage/{urllib.parse.quote(resource_name, safe='')}"
    status, payload = client.request("GET", path, expected=(200, 404))
    if status == 404:
        return None
    if not isinstance(payload, dict):
        raise RuntimeError(f"user-storage GET returned unexpected payload: {payload!r}")
    return payload


def create_resource(client: GrafanaClient, namespace: str, resource_name: str, user_uid: str) -> None:
    path = f"/apis/userstorage.grafana.app/v0alpha1/namespaces/{namespace}/user-storage/"
    body = {
        "metadata": {
            "name": resource_name,
            "labels": {
                "user": user_uid,
                "service": "grafana-help-flags",
            },
        },
        "spec": {
            "data": dict(DESIRED_FLAGS),
        },
    }
    client.request("POST", path, body=body, expected=(200, 201))


def patch_resource(client: GrafanaClient, namespace: str, resource_name: str) -> None:
    path = f"/apis/userstorage.grafana.app/v0alpha1/namespaces/{namespace}/user-storage/{urllib.parse.quote(resource_name, safe='')}"
    body = {
        "spec": {
            "data": dict(DESIRED_FLAGS),
        }
    }
    client.request(
        "PATCH",
        path,
        body=body,
        headers={"Content-Type": "application/merge-patch+json"},
        expected=(200,),
    )


def verify_flags(resource: dict | None) -> None:
    if not isinstance(resource, dict):
        raise RuntimeError("verification failed: user-storage resource is missing")
    spec = resource.get("spec")
    if not isinstance(spec, dict):
        raise RuntimeError("verification failed: resource spec missing")
    data = spec.get("data")
    if not isinstance(data, dict):
        raise RuntimeError("verification failed: resource spec.data missing")

    missing = {k: v for k, v in DESIRED_FLAGS.items() if data.get(k) != v}
    if missing:
        raise RuntimeError(f"verification failed: expected flags not present: {json.dumps(missing, sort_keys=True)}")


def main() -> int:
    try:
        args = parse_args()
        client = GrafanaClient(args.url, args.cookie_header)
        login_if_requested(client, args)

        user = get_current_user(client)
        user_uid = user.get("uid") or str(user.get("id") or "")
        if not user_uid:
            raise RuntimeError(f"could not determine current user uid from /api/user: {json.dumps(user, sort_keys=True)}")

        namespace = discover_namespace(client, args.namespace)
        resource_name = f"grafana-help-flags:{user_uid}"

        existing = get_resource(client, namespace, resource_name)
        if existing is None:
            create_resource(client, namespace, resource_name, user_uid)
            action = "created"
        else:
            patch_resource(client, namespace, resource_name)
            action = "patched"

        verified = get_resource(client, namespace, resource_name)
        verify_flags(verified)

        print(f"OK: {action} {resource_name} in namespace {namespace}")
        print("Seeded flags:")
        for key in sorted(DESIRED_FLAGS):
            print(f"  {key}=true")
        print("Notes:")
        print("  backend-persisted via /apis/userstorage.grafana.app")
        print("  not attempting browser-localStorage-only promos such as datasources.settings.cloudInfoBox.isDismissed")
        return 0
    except Exception as err:
        print(f"ERROR: {err}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    sys.exit(main())
