"""Configuration models for the A360 Control Room connection.

Kept deliberately small and declarative: every derived value is a property, so a
config object can be built from a dict, a CLI namespace, or env vars without any
of the callers knowing which source won.
"""

from enum import StrEnum

from pydantic import BaseModel, ConfigDict, Field, SecretStr, field_validator, model_validator

DEFAULT_REQUEST_TIMEOUT_SECONDS = 60
DEFAULT_TOKEN_TTL_SECONDS = 1200  # Control Room default is 20 minutes
TOKEN_REFRESH_SKEW_SECONDS = 60  # renew this long before nominal expiry
MAX_CONCURRENT_SESSIONS_PER_ACCOUNT = 5  # Control Room hard limit


class AuthType(StrEnum):
    """How this server proves who it is to the Control Room."""

    API_KEY = "api_key"
    PASSWORD = "password"


def _revealed(secret: SecretStr | None) -> str | None:
    """Plaintext behind a `SecretStr`, or None. The single deliberate unwrap."""
    return secret.get_secret_value() if secret is not None else None


class A360AuthConfig(BaseModel):
    """Credentials for one Control Room account.

    `apiKey` and `password` are mutually exclusive in the v2 authentication API
    (the spec declares them under `anyOf`), so the model rejects both up front
    rather than letting the Control Room return an opaque 400.

    Secrets are `SecretStr` and `hide_input_in_errors` is on: `cli.main` logs a
    failed config build verbatim to stderr and to the rotating log file, and
    pydantic otherwise echoes the raw input dict into the error message.
    """

    model_config = ConfigDict(hide_input_in_errors=True)

    username: str = Field(..., description="Control Room username")
    api_key: SecretStr | None = Field(default=None, description="API key from My settings")
    password: SecretStr | None = Field(default=None, description="Password; prefer api_key")
    multiple_login: bool = Field(
        default=False, description="Allow concurrent sessions for this account"
    )

    @field_validator("username")
    @classmethod
    def _username_not_blank(cls, value: str) -> str:
        stripped = value.strip()
        if not stripped:
            raise ValueError("username must not be blank")
        return stripped

    @model_validator(mode="after")
    def _exactly_one_secret(self) -> "A360AuthConfig":
        has_api_key = bool((_revealed(self.api_key) or "").strip())
        has_password = bool((_revealed(self.password) or "").strip())
        if has_api_key and has_password:
            raise ValueError(
                "api_key and password cannot be used together — the v2 authentication "
                "API accepts exactly one"
            )
        if not has_api_key and not has_password:
            raise ValueError("one of api_key or password is required")
        return self

    @property
    def api_key_value(self) -> str | None:
        """Plaintext API key. Callers must not log or echo the result."""
        return _revealed(self.api_key) or None

    @property
    def password_value(self) -> str | None:
        """Plaintext password. Callers must not log or echo the result."""
        return _revealed(self.password) or None

    @property
    def auth_type(self) -> AuthType:
        return AuthType.API_KEY if self.api_key_value else AuthType.PASSWORD

    def to_auth_request(self) -> dict:
        """Body for `POST /v2/authentication`."""
        body: dict = {"username": self.username, "multipleLogin": self.multiple_login}
        if self.api_key_value:
            body["apiKey"] = self.api_key_value
        else:
            body["password"] = self.password_value
        return body


class ServerConfig(BaseModel):
    """Everything the server needs to talk to one Control Room."""

    # Nested auth is validated from a raw dict here too — same logging surface.
    model_config = ConfigDict(hide_input_in_errors=True)

    control_room_url: str = Field(..., description="Base Control Room URL")
    auth: A360AuthConfig
    allow_writes: bool = Field(
        default=False, description="Opt-in per instance; absence must never mean writable"
    )
    request_timeout: int = Field(default=DEFAULT_REQUEST_TIMEOUT_SECONDS, gt=0)
    ca_bundle: str | None = Field(
        default=None, description="Path to a CA bundle for private/self-signed certs"
    )
    tls_verify: bool = Field(default=True, description="Set False only as an explicit opt-out")

    @field_validator("control_room_url")
    @classmethod
    def _normalize_url(cls, value: str) -> str:
        stripped = value.strip().rstrip("/")
        if not stripped:
            raise ValueError("control_room_url must not be blank")
        if not stripped.startswith(("http://", "https://")):
            raise ValueError("control_room_url must start with http:// or https://")
        return stripped

    @property
    def api_url(self) -> str:
        """Base for versioned API paths — A360 has no tenant path segment."""
        return self.control_room_url

    @property
    def auth_url(self) -> str:
        return f"{self.api_url}/v2/authentication"

    @property
    def token_refresh_url(self) -> str:
        return f"{self.api_url}/v2/authentication/token"

    @property
    def verify(self) -> object:
        """Value for the `verify=` argument of the HTTP session."""
        if not self.tls_verify:
            return False
        return self.ca_bundle or True
