# src/gcp_mysql/utils/factory.py

from __future__ import annotations

import json
import logging
import os
import tempfile
from json import JSONDecodeError
from typing import (
    Optional, 
    Type,
    TYPE_CHECKING,
)

try:
    from google.cloud import secretmanager  # type: ignore
    from google.auth.exceptions import RefreshError  # type: ignore
    SECRET_MANAGER_AVAILABLE = True
except ImportError:  # pragma: no cover
    SECRET_MANAGER_AVAILABLE = False
    secretmanager = None  # type: ignore[assignment]
    RefreshError = None  # type: ignore[assignment, misc]

if TYPE_CHECKING:
    from ..service import MySQLService

logger = logging.getLogger(__name__)

SUPPORTED_CONNECTION_MODES = {"cloudsql", "tcp"}


def from_gcp_secret(
    cls: Type["MySQLService"],
    *,
    project_id: str,
    secret_id: str,
    version_id: str = "latest",
    ssl_ca_secret_id: Optional[str] = None,
) -> "MySQLService":
    """
    Create a MySQLService using credentials from GCP Secret Manager.

    Note:
        This factory relies on environment variables to determine connection mode
        and host configuration.

    Connection mode is controlled EXCLUSIVELY by environment variables:

    Local development (Cloud SQL Proxy):
        MYSQL_CONNECTION_MODE=tcp
        MYSQL_HOST=127.0.0.1
        MYSQL_PORT=3306

    Cloud Run (prod):
        MYSQL_CONNECTION_MODE=cloudsql
        CLOUDSQL_INSTANCE=project:region:instance

    The secret must contain ONLY credentials:
        {
          "USER": "...",
          "PASSWORD": "...",
          "DATABASE": "...",
          "PORT": 3306
        }
    """
    # Defensive guard: this is intended to be bound via classmethod on MySQLService.
    if not isinstance(cls, type):
        raise TypeError("from_gcp_secret must be called as a classmethod (e.g., MySQLService.from_gcp_secret(...))")

    if not SECRET_MANAGER_AVAILABLE:
        raise RuntimeError("google-cloud-secret-manager is not installed")

    # -------------------------
    # Load secret
    # -------------------------
    secret_path = f"projects/{project_id}/secrets/{secret_id}/versions/{version_id}"

    try:
        client = secretmanager.SecretManagerServiceClient()
    except Exception as e:
        # Check if this is a RefreshError (GCP authentication failure)
        if RefreshError is not None and isinstance(e, RefreshError):
            logger.exception("GCP authentication failed when creating Secret Manager client")
            raise RuntimeError(
                "GCP authentication failed. Please run `gcloud auth application-default login` to authenticate. "
                f"Original error: {e}"
            ) from e
        logger.exception("Failed to create Secret Manager client")
        raise RuntimeError(f"Failed to create Secret Manager client: {e}") from e

    try:
        response = client.access_secret_version(request={"name": secret_path})
        raw = response.payload.data.decode("utf-8")
        cfg = json.loads(raw)
    except (UnicodeDecodeError, JSONDecodeError, ValueError) as e:
        logger.exception("Failed to decode/parse MySQL secret '%s'", secret_id)
        raise RuntimeError(f"Failed to decode/parse MySQL secret '{secret_id}': {e}") from e
    except Exception as e:
        # Check if this is a RefreshError (GCP authentication failure)
        if RefreshError is not None and isinstance(e, RefreshError):
            logger.exception("GCP authentication failed when accessing MySQL secret '%s'", secret_id)
            raise RuntimeError(
                f"GCP authentication failed when accessing secret '{secret_id}'. "
                "Please run `gcloud auth application-default login` to authenticate. "
                f"Original error: {e}"
            ) from e
        logger.exception("Failed to load MySQL secret '%s'", secret_id)
        raise RuntimeError(f"Failed to load MySQL secret '{secret_id}': {e}") from e

    # -------------------------
    # Validate secret contents
    # -------------------------
    try:
        user = cfg["USER"]
        password = cfg["PASSWORD"]
        database = cfg["DATABASE"]
        port = int(cfg.get("PORT", 3306))
    except KeyError as e:
        raise RuntimeError(f"Missing required key in MySQL secret: {e}") from e
    except (TypeError, ValueError) as e:
        raise RuntimeError(f"Invalid PORT value in MySQL secret: {e}") from e

    # -------------------------
    # Determine connection mode
    # -------------------------
    mode = os.getenv("MYSQL_CONNECTION_MODE", "cloudsql").strip().lower()
    if mode not in SUPPORTED_CONNECTION_MODES:
        raise RuntimeError(
            f"Invalid MYSQL_CONNECTION_MODE={mode}. Expected one of {sorted(SUPPORTED_CONNECTION_MODES)}"
        )

    kwargs: dict = {
        "user": user,
        "password": password,
        "database": database,
        "port": port,
    }

    if mode == "cloudsql":
        instance = os.getenv("CLOUDSQL_INSTANCE")
        if not instance:
            raise RuntimeError("CLOUDSQL_INSTANCE must be set for cloudsql mode")

        kwargs["unix_socket"] = f"/cloudsql/{instance}"
        logger.debug("MySQL connection mode: Cloud SQL unix socket (%s)", instance)

    elif mode == "tcp":
        host = os.getenv("MYSQL_HOST", "127.0.0.1").strip()
        port_env = os.getenv("MYSQL_PORT")
        if port_env:
            try:
                port = int(port_env)
            except ValueError as e:
                raise RuntimeError(f"Invalid MYSQL_PORT={port_env}: {e}") from e

        kwargs["host"] = host
        kwargs["port"] = port
        logger.debug("MySQL connection mode: TCP (%s:%s)", host, port)

    # -------------------------
    # Optional SSL CA support
    # -------------------------
    if ssl_ca_secret_id:
        ssl_path = f"projects/{project_id}/secrets/{ssl_ca_secret_id}/versions/{version_id}"
        try:
            ssl_resp = client.access_secret_version(request={"name": ssl_path})

            # IMPORTANT: PyMySQL expects a *file path* for ssl["ca"], not PEM contents.
            # Write the CA cert bytes to a temp file and pass the path through.
            ca_bytes = ssl_resp.payload.data
            with tempfile.NamedTemporaryFile(prefix="gcp_mysql_ca_", suffix=".pem", delete=False) as f:
                f.write(ca_bytes)
                kwargs["ssl_ca_path"] = f.name

            logger.debug("Loaded MySQL SSL CA from secret '%s' into temp file", ssl_ca_secret_id)
        except Exception as e:
            # Check if this is a RefreshError (GCP authentication failure)
            if RefreshError is not None and isinstance(e, RefreshError):
                logger.warning(
                    "GCP authentication failed when accessing optional SSL CA secret '%s'. "
                    "Please run `gcloud auth application-default login` to authenticate. Error: %s",
                    ssl_ca_secret_id, e
                )
            else:
                logger.warning("Failed to load optional SSL CA secret '%s': %s", ssl_ca_secret_id, e)

    # -------------------------
    # Final safety guard
    # -------------------------
    if "unix_socket" in kwargs and "host" in kwargs:
        raise RuntimeError("Invalid MySQL configuration: host and unix_socket both set")

    # -------------------------
    # Create service
    # -------------------------
    try:
        return cls(**kwargs)
    except TypeError as e:
        logger.exception("Failed to create MySQLService due to invalid constructor args")
        raise
    except Exception:
        logger.exception("Failed to create MySQLService")
        raise