# The Okta software accompanied by this notice is provided pursuant to the following terms:
# Copyright © 2025-Present, Okta, Inc.
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
# License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS
# IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
# coding: utf-8

"""
Okta Admin Management

Allows customers to easily access the Okta Management APIs

The version of the OpenAPI document: 5.1.0
Contact: devex-public@okta.com
Generated by OpenAPI Generator (https://openapi-generator.tech)

Do not edit the class manually.
"""  # noqa: E501
import logging

from typing import Dict, Any, Optional

# Import the base and subclass models
from okta.models.application import Application
from okta.models.active_directory_application import ActiveDirectoryApplication
from okta.models.auto_login_application import AutoLoginApplication
from okta.models.basic_auth_application import BasicAuthApplication
from okta.models.bookmark_application import BookmarkApplication
from okta.models.browser_plugin_application import BrowserPluginApplication
from okta.models.open_id_connect_application import OpenIdConnectApplication
from okta.models.saml11_application import Saml11Application
from okta.models.saml_application import SamlApplication
from okta.models.secure_password_store_application import SecurePasswordStoreApplication
from okta.models.ws_federation_application import WsFederationApplication

logger = logging.getLogger(__name__)


class ApplicationJsonConverter:
    """
    Custom JSON converter/factory for Application that handles null, empty, and unknown signOnMode values.

    Routing logic:
    - null (None) → ActiveDirectoryApplication (AD integrations have null signOnMode)
    - "" (empty string) → Application base class with original value preserved
    - Known modes (AUTO_LOGIN, SAML_2_0, etc.) → Specific subclasses (case-insensitive)
    - Unknown modes (e.g., FUTURE_MODE) → Application base class with original value preserved

    Round-trip behavior:
    - Unknown values stored in _original_sign_on_mode attribute
    - Restored in to_dict() output for API fidelity
    - None (for missing field) routes to ActiveDirectoryApplication
    - Empty string is preserved as-is for round-trip integrity

    Case handling:
    - Known modes are normalized to uppercase (e.g., "auto_login" → "AUTO_LOGIN")
    - Unknown modes preserve original casing (e.g., "Future_Mode" stays "Future_Mode")
    """

    # All known signOnMode values from ApplicationSignOnMode enum
    KNOWN_SIGN_ON_MODES = {
        "AUTO_LOGIN",
        "BASIC_AUTH",
        "BOOKMARK",
        "BROWSER_PLUGIN",
        "OPENID_CONNECT",
        "SAML_1_1",
        "SAML_2_0",
        "SECURE_PASSWORD_STORE",
        "WS_FEDERATION",
    }

    SIGN_ON_MODE_MAPPING = {
        "AUTO_LOGIN": AutoLoginApplication,
        "BASIC_AUTH": BasicAuthApplication,
        "BOOKMARK": BookmarkApplication,
        "BROWSER_PLUGIN": BrowserPluginApplication,
        "OPENID_CONNECT": OpenIdConnectApplication,
        "SAML_1_1": Saml11Application,
        "SAML_2_0": SamlApplication,
        "SECURE_PASSWORD_STORE": SecurePasswordStoreApplication,
        "WS_FEDERATION": WsFederationApplication,
    }

    @classmethod
    def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Application]:
        """
        Reads the dictionary representation of the Application object, using signOnMode
        to determine the correct type. Handles the special case where signOnMode is null
        (Active Directory applications) or unknown (future sign-on modes).
        """
        if obj is None:
            return None

        # Get the signOnMode value to determine the concrete type
        sign_on_mode = obj.get("signOnMode")
        original_sign_on_mode = None

        # Make a copy to avoid modifying the input
        obj_copy = dict(obj)

        # Determine the target type based on signOnMode
        if sign_on_mode is None:
            # null signOnMode indicates an Active Directory application
            target_type = ActiveDirectoryApplication
        elif sign_on_mode == "":
            # Empty string signOnMode is treated as unknown (not ActiveDirectory)
            original_sign_on_mode = sign_on_mode
            obj_copy["signOnMode"] = None
            target_type = Application
        elif str(sign_on_mode).upper() in cls.KNOWN_SIGN_ON_MODES:
            # Known sign-on mode - map to specific class (or base Application for MFA_AS_SERVICE)
            target_type = cls.SIGN_ON_MODE_MAPPING[str(sign_on_mode).upper()]
            obj_copy["signOnMode"] = str(
                sign_on_mode
            ).upper()  # Normalize for validation
        else:
            # Unknown sign-on mode - preserve original value and set to None for validation
            logger.debug(
                f"Unknown signOnMode '{sign_on_mode}' encountered, "
                f"routing to base Application class"
            )
            original_sign_on_mode = sign_on_mode
            obj_copy["signOnMode"] = None
            target_type = Application

        # Use Pydantic's model_validate to avoid recursion
        # (calling from_dict would trigger the converter again in subclasses)
        instance = target_type.model_validate(obj_copy)

        # Validate consistency (in dev/test mode)
        if isinstance(instance, Application) and not isinstance(
            instance, type(instance).__bases__[0]
        ):
            # Log warning if subclass instantiated but fields seem inconsistent
            logger.debug(f"Routed {sign_on_mode} to {type(instance).__name__}")

        # Store the original unknown sign-on mode if present
        # For known modes, set to None to indicate no preservation needed
        if original_sign_on_mode is not None:
            # Use object.__setattr__ to bypass Pydantic's validation
            object.__setattr__(
                instance, "_original_sign_on_mode", original_sign_on_mode
            )
        else:
            object.__setattr__(instance, "_original_sign_on_mode", None)

        return instance

    @classmethod
    def to_dict(cls, value: Application) -> Optional[Dict[str, Any]]:
        """
        Writes the dictionary representation of the Application object.
        Uses Pydantic's model_dump directly to avoid recursion.
        Restores original unknown signOnMode values for round-trip fidelity.
        """
        if value is None:
            return None

        # Derive from model configuration
        excluded_fields = {
            field_name
            for field_name, field_info in Application.model_fields.items()
            if field_info.exclude
            or (
                field_info.json_schema_extra
                and field_info.json_schema_extra.get("readOnly")
            )
        }

        _dict = value.model_dump(
            by_alias=True,
            exclude=excluded_fields,
            exclude_none=True,
            mode="json",  # Serialize enums to their values
        )

        # Restore original unknown signOnMode if it was preserved
        if (
            hasattr(value, "_original_sign_on_mode")
            and value._original_sign_on_mode is not None
        ):
            _dict["signOnMode"] = value._original_sign_on_mode
        # Ensure signOnMode is serialized as string (in case mode='json' didn't handle it)
        elif "signOnMode" in _dict and hasattr(_dict["signOnMode"], "value"):
            _dict["signOnMode"] = _dict["signOnMode"].value

        # NOTE: Pydantic's model_dump(mode='json') handles most nested BaseModel serialization.
        # However, subclass-specific fields (settings, credentials, name) need manual handling
        # because they're not in the base Application schema and model_dump doesn't call
        # their custom to_dict() methods which may have additional logic.

        # Handle subclass-specific fields that may have custom serialization logic
        if hasattr(value, "settings") and value.settings:
            if not isinstance(value.settings, dict):
                _dict["settings"] = value.settings.to_dict()
            else:
                _dict["settings"] = value.settings

        if hasattr(value, "credentials") and value.credentials:
            if not isinstance(value.credentials, dict):
                _dict["credentials"] = value.credentials.to_dict()
            else:
                _dict["credentials"] = value.credentials

        if hasattr(value, "name") and value.name:
            _dict["name"] = value.name

        return _dict
