#
# # Copyright © 2026 Peak AI Limited. or its affiliates. All Rights Reserved.
# #
# # Licensed under the Apache License, Version 2.0 (the "License"). You
# # may not use this file except in compliance with the License. A copy of
# # the License is located at:
# #
# # https://github.com/PeakBI/peak-sdk/blob/main/LICENSE
# #
# # or in the "license" file accompanying this file. This file 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.
# #
# # This file is part of the peak-sdk.
# # see (https://github.com/PeakBI/peak-sdk)
# #
# # You should have received a copy of the APACHE LICENSE, VERSION 2.0
# # along with this program. If not, see <https://apache.org/licenses/LICENSE-2.0>
#
"""Deploy applications or individual blocks to the Peak platform."""

from __future__ import annotations

import os
from pathlib import Path
from typing import Any

from peak.press import apps, blocks
from peak.press.wrappers.deploy.config.deployment_config import (
    add_block_swap_configuration,
    find_deployment,
    prepare_revision_body,
)
from peak.press.wrappers.deploy.utils.deploy_block import (
    create_new_block_deployment,
    fetch_block_config,
    find_block_deployment,
    prepare_block_revision_body,
    update_existing_block_deployment,
)
from peak.press.wrappers.deploy.utils.parameters_utils import (
    parse_build_and_run_parameters,
    remove_app_params_from_blocks,
)
from peak.press.wrappers.deploy.utils.plugin_utils import process_plugin_paths
from peak.template import load_template
from peak.tools.logging import LogLevel, PeakLogger, get_logger

logger: PeakLogger = get_logger(level=LogLevel.INFO)


def _get_apps_client() -> Any:
    """Get the apps client lazily."""
    return apps.get_client()


def _get_blocks_client() -> Any:
    """Get the blocks client lazily."""
    return blocks.get_client()


def deep_merge_dicts(original_dict: dict[str, Any], override_dict: dict[str, Any]) -> dict[str, Any]:
    """Recursively merge two dictionaries, with override_dict taking precedence.

    Args:
        original_dict: The base dictionary
        override_dict: Dictionary with values to override

    Returns:
        Merged dictionary with override values applied
    """
    # Create a copy of the original dict to avoid modifying it directly
    result = original_dict.copy()
    # Iterate through each key-value pair in the override dict
    for key, value in override_dict.items():
        # If the key exists in the original and both values are dictionaries, recurse
        if key in result and isinstance(result[key], dict) and isinstance(value, dict):
            result[key] = deep_merge_dicts(result[key], value)
        # Otherwise, just use the value from the override dict
        else:
            result[key] = value

    return result


def fetch_app_config(app_spec_id: str, app_version: str) -> tuple[dict[str, Any], list[dict[str, Any]]]:
    """Fetch application configuration and parameters.

    Args:
        app_spec_id: The application specification ID
        app_version: The application version

    Returns:
        Tuple of (app_parameters, app_blocks)
    """
    apps_client = _get_apps_client()
    app_spec_release_description = apps_client.describe_spec_release(
        spec_id=app_spec_id,
        version=app_version,
    )

    app_parameters = parse_build_and_run_parameters(app_spec_release_description["parameters"])
    app_blocks = app_spec_release_description["config"]

    return app_parameters, app_blocks


def fetch_block_parameters(app_blocks: list[dict[str, Any]]) -> dict[str, Any]:
    """Fetch parameters for all blocks in the application.

    Args:
        app_blocks: The application blocks configuration

    Returns:
        Dictionary of block parameters by block name
    """
    blocks_client = _get_blocks_client()
    block_parameters = {}

    for block in app_blocks:
        block_name = blocks_client.describe_spec(
            spec_id=block["id"],
        )[
            "metadata"
        ]["name"]
        block_spec_parameters = blocks_client.describe_spec_release(
            spec_id=block["id"],
            version=block["release"]["version"],
        )["parameters"]
        block_parameters[block_name] = parse_build_and_run_parameters(block_spec_parameters)

    return block_parameters


def update_existing_deployment(deployment_id: str, revision_body: dict[str, Any]) -> None:
    """Update an existing deployment with a new revision.

    Args:
        deployment_id: The deployment ID to update
        revision_body: The revision configuration

    Raises:
        ValueError: If revision creation fails
        KeyError: If required keys are missing
        RuntimeError: If deployment update fails
    """
    apps_client = _get_apps_client()
    try:
        apps_client.create_deployment_revision(
            deployment_id=deployment_id,
            body=revision_body,
        )
        logger.info("✅ Successfully updated the application with id: %s", deployment_id)
    except (ValueError, KeyError, RuntimeError):
        logger.exception("Failed to create the deployment revision")
        raise


def create_new_deployment(final_body: dict[str, Any]) -> None:
    """Create a new deployment.

    Args:
        final_body: The deployment configuration

    Raises:
        ValueError: If deployment creation fails
        KeyError: If required keys are missing
        RuntimeError: If deployment fails
    """
    apps_client = _get_apps_client()
    try:
        apps_client.create_deployment(
            body=final_body,
        )
        logger.info("✅ Successfully deployed the application.")
    except (ValueError, KeyError, RuntimeError):
        logger.exception("Failed to create the new deployment")
        raise


def deploy(
    config_path: str,
    spec_id: str | None = None,
    version: str | None = None,
    *,
    deploy_app: bool = True,
) -> None:
    """Deploy an application or individual block to the Peak platform.

    Args:
        config_path: Path to the deployment specification YAML file
        spec_id: Override the app/block ID from the config file
        version: Override the app/block version from the config file
        deploy_app: True for app deployment, False for single block

    Raises:
        ValueError: If config_path is empty
        FileNotFoundError: If deployment config file not found
    """
    suffix = os.getenv("SUFFIX", "") or os.getenv("SPEC_SUFFIX", "")

    if not config_path:
        msg = "Config path must be provided"
        raise ValueError(msg)

    # Convert to Path object to handle path resolution
    config_path_obj = Path(config_path)

    # If the path isn't absolute, check if it exists relative to CWD
    if not config_path_obj.is_absolute():
        config_path_obj = Path.cwd() / config_path_obj

    # Check if file exists
    if not config_path_obj.exists():
        msg = f"Deployment config file not found at: {config_path_obj}"
        raise FileNotFoundError(msg)

    # Convert back to string for load_template
    resolved_config_path = str(config_path_obj)

    logger.debug("Loading deployment config from %s", resolved_config_path)

    # Load deployment configuration
    deployment_config = load_template(
        file=resolved_config_path,
        params={
            "SUFFIX": suffix,
        },
    )

    if spec_id:
        deployment_config["spec"]["id"] = spec_id

    if version:
        deployment_config["spec"]["release"]["version"] = version

    if deploy_app:
        # App deployment logic (existing functionality)
        _deploy_app(deployment_config)
    else:
        # Single block deployment logic (new functionality)
        _deploy_block(deployment_config)


def _deploy_app(deployment_config: dict[str, Any]) -> None:
    """Deploy an application with its associated blocks.

    Args:
        deployment_config: The deployment configuration
    """
    # Get application configuration
    app_parameters, app_blocks = fetch_app_config(
        deployment_config["spec"]["id"],
        deployment_config["spec"]["release"]["version"],
    )

    app_blocks = add_block_swap_configuration(deployment_config, app_blocks)

    # Get block parameters
    block_parameters = fetch_block_parameters(app_blocks)

    # Process parameters
    filtered_block_parameters = remove_app_params_from_blocks(app_parameters, block_parameters)
    final_block_parameters = deep_merge_dicts(filtered_block_parameters, deployment_config["parameters"])
    final_app_parameters = deep_merge_dicts(app_parameters, deployment_config["appParameters"])

    # Create final deployment body
    final_body = deployment_config.copy()
    final_body["appParameters"] = final_app_parameters
    final_body["parameters"] = final_block_parameters

    # Process plugins
    process_plugin_paths(deployment_config, final_body)

    # Check if deployment already exists
    apps_client = _get_apps_client()
    deployments = apps_client.list_deployments(
        name=deployment_config["metadata"]["name"],
        return_iterator=False,
    )

    deployment = find_deployment(deployments["deployments"], deployment_config["spec"]["id"])

    # Update or create deployment
    if deployment:
        revision_body = prepare_revision_body(final_body)
        update_existing_deployment(deployment["id"], revision_body)
    else:
        create_new_deployment(final_body)


def _deploy_block(deployment_config: dict[str, Any]) -> None:
    """Deploy a single block without an app context.

    Args:
        deployment_config: The deployment configuration
    """
    # Get block configuration
    block_parameters, block_metadata = fetch_block_config(
        deployment_config["spec"]["id"],
        deployment_config["spec"]["release"]["version"],
    )

    # Merge block parameters with deployment config parameters
    final_block_parameters = deep_merge_dicts(block_parameters, deployment_config.get("parameters", {}))

    # Create final deployment body for block
    final_body = deployment_config.copy()
    final_body["parameters"] = final_block_parameters

    # Remove appParameters as they don't apply to block deployments
    if "appParameters" in final_body:
        del final_body["appParameters"]

    # Process plugins for block deployment
    process_plugin_paths(deployment_config, final_body)

    # Check if block deployment already exists
    blocks_client = _get_blocks_client()
    deployments = blocks_client.list_deployments(
        name=deployment_config["metadata"]["name"],
        return_iterator=False,
    )

    deployment = find_block_deployment(deployments["deployments"], deployment_config["spec"]["id"])

    # Update or create block deployment
    if deployment:
        revision_body = prepare_block_revision_body(final_body)
        update_existing_block_deployment(deployment["id"], revision_body)
    else:
        create_new_block_deployment(final_body)
