from __future__ import annotations

import hashlib
import uuid
from datetime import datetime
from enum import Enum
from typing import Any

import dataiku
from flask import Blueprint, g, jsonify, request
from pydantic import ValidationError
from werkzeug.exceptions import PreconditionFailed

from backend.config import get_admin_uploads_managedfolder_id
from backend.models.admin_config import ADMIN_CONFIG_UPDATE_ADAPTER
from backend.services.admin_config_service import get_default_config_for_mode, read_config, write_config
from backend.socket import socketio
from backend.utils.client_utils import current_request_login, perform_http
from backend.utils.embed_url_utils import (
    validate_embed_origins,
    validate_embed_path_prefixes,
    validate_embed_query_parameters,
)
from backend.utils.logger_utils import log_http_request
from backend.utils.logging_utils import get_logger
from backend.utils.permissions_utils import has_project_write_access

admin_bp = Blueprint("admin", __name__, url_prefix="/admin")

logger = get_logger(__name__)

LOOK_AND_FEEL_KEYS = [
    "leftPanelLogoPath",
    "homepageImagePath",
    "homepageTitle",
    "chatFooterText",
    "themeColor",
    "themeColorMode",
    "customCssPath",
    "customFontEnabled",
    "adminUploadsManagedFolder",
]

ASSET_KEYS = ["leftPanelLogoPath", "homepageImagePath", "customCssPath"]


@admin_bp.before_request
def admin_before_request():
    login = current_request_login() or ""
    logger.debug("Checking admin access for request to %s and login %s", request.path, login)
    if not login:
        logger.warning("Unauthenticated access attempt on admin endpoint")
        return jsonify({"error": "Authentication required"}), 401

    if not has_project_write_access(dataiku.default_project_key(), login):
        logger.warning("Unauthorized admin access attempt by user '%s'", login)
        return jsonify({"error": "Admin access required"}), 403

    return None


def _normalize_config_value(value: Any) -> str:
    if isinstance(value, dict) and "path" in value:
        return value.get("path") or ""
    if isinstance(value, str):
        return value
    return ""


def _is_empty_value(value: Any) -> bool:
    if value is None:
        return True
    if value == "":
        return True
    if isinstance(value, dict):
        path = value.get("path")
        return path is None or path == ""
    return False


def _normalize_config_response(config: dict[str, Any]) -> dict[str, Any]:
    payload = dict(config)
    for key in LOOK_AND_FEEL_KEYS:
        value = payload.get(key)
        if isinstance(value, dict) and "path" in value:
            payload[key] = value.get("path")
    return payload


class FileSystemConnectionType(Enum):
    FILESYSTEM = "Filesystem"
    EC2 = "EC2"
    FTP = "FTP"
    SSH = "SSH"
    AZURE = "Azure"
    GCS = "GCS"
    HDFS = "HDFS"
    SHAREPOINT_ONLINE = "SharePointOnline"


# IMPORTANT: Has to be aligned with connections supported by 'semantic-models-lab'
# HELA: is this is the actual list of connections supported by semantic-models-lab or just a sample? Shouldn't be supporting all SQL connections?
class SQLConnectionType(Enum):
    POSTGRE_SQL = "PostgreSQL"
    MY_SQL = "MySQL"
    SNOWFLAKE = "Snowflake"
    BIG_QUERY = "BigQuery"
    SQL_SERVER = "SQLServer"
    ORACLE = "Oracle"
    REDSHIFT = "Redshift"
    SYNAPSE = "Synapse"
    TERADATA = "Teradata"
    VERTICA = "Vertica"
    GREENPLUM = "Greenplum"
    JDBC = "JDBC"
    ATHENA = "Athena"
    DATABRICKS = "Databricks"
    SAPHANA = "SAPHANA"
    NETEZZA = "Netezza"
    HIVE_SERVER_2 = "hiveserver2"


def get_connections_map(conn_types: type[Enum]) -> dict[str, list[str]]:
    """
    Retrieves a list of connections of type 'connection_type' using the Python API
        :param connection_type: str: the type of connection to list ('SQL', 'Filesystem')
    """
    connection_map = {}
    client = dataiku.api_client()

    for conn_type in conn_types:
        try:
            dss_connections = client.list_connections_names(conn_type.value)
            if dss_connections:
                for connection_name in dss_connections:
                    connection_map.setdefault(conn_type.value, []).append(connection_name)
        except Exception as e:
            logger.error(f"Error processing connection type {conn_type.value}: {e}")
    return connection_map


# returns a list of workspaces, each workspace is a dict with:
# id: workspace["workspaceKey"]
# displayName: workspace["displayName"]
# description: workspace["description"]
def list_workspaces():
    return dataiku.api_client().list_workspaces()


# Recursively build folder hierarchy
def _build_folder_tree(folder, parent_path="", level=0):
    """
    Recursively build a flat list with hierarchy information.
    Each item contains: id, name, path, level, hasChildren
    """
    try:
        folder_id = folder.id
        folder_name = folder.get_name()
        current_path = f"{parent_path}/{folder_name}" if parent_path else folder_name

        result = [
            {
                "id": folder_id,
                "name": folder_name,
                "path": current_path,
                "level": level,
                "hasChildren": False,  # Will be updated if children exist
            }
        ]

        # Get children
        try:
            children = folder.list_child_folders()
            if children:
                result[0]["hasChildren"] = True
                for child in children:
                    result.extend(_build_folder_tree(child, current_path, level + 1))
        except Exception as e:
            logger.exception(f"Could not list children for folder {folder_name}: {e}")

        return result
    except Exception as e:
        logger.error(f"Error building folder tree: {e}")
        return []


def list_projects_folders():
    """
    Returns a flat list of all folders with hierarchy information.
    Each folder has: id, name, path, level, hasChildren
    """
    try:
        client = dataiku.api_client()
        root_folder = client.get_root_project_folder()

        # Start with root
        folders = [{"id": "ROOT", "name": "Root", "path": "/", "level": 0, "hasChildren": True}]

        # Add all children recursively
        try:
            children = root_folder.list_child_folders()
            for child in children:
                folders.extend(_build_folder_tree(child, "", 1))
        except Exception as e:
            logger.exception(f"Could not list root children: {e}")

        return folders
    except Exception as e:
        logger.error(f"Error listing project folders: {e}")
        return []


def list_connections_from_type(connection_type):
    """
    Retrieves a list of connections of type 'connection_type'
        :param connection_type: str: the type of connection to list ('SQL', 'Filesystem')
    """
    connections = []
    client = dataiku.api_client()
    project_key = client.get_default_project().project_key
    if connection_type == "Filesystem":
        endpoint = "/flow/recipes/get-managed-folder-options-no-context"
    elif connection_type == "SQL":
        endpoint = "/flow/recipes/get-managed-dataset-options-no-context"
    else:
        raise ValueError(f"'list_connections_by_type' doesn't support `{connection_type}`.")

    try:
        response = perform_http(
            client=client,
            method="POST",
            path=endpoint,
            params={"projectKey": project_key},
        )
        dss_connections_data = response.json()
        connections_list = dss_connections_data.get("connections", [])

        # Transform DSS connection data to our format, preserving the original mapping logic
        for conn in connections_list:
            conn_id = conn.get("id", "")
            conn_name = conn.get("connectionName", conn_id)
            conn_display_name = conn.get("connectionDisplayName", conn_name)
            conn_type = conn.get("connectionType", "")

            # Preserve original mapping: connections use name only,
            # other types include type suffix
            if conn_type == connection_type:
                label = conn_display_name if conn_display_name else conn_name
            else:
                # For other connection types (For 'Filesystem': EC2, FTP, SSH, Azure, GCS, HDFS, SharePointOnline)
                label = f"{conn_name} ({conn_type})"

            connections.append(
                {
                    "id": conn_id,
                    "label": label,
                    "type": conn_type,
                }
            )
        return connections
    except Exception as e:
        raise Exception(f"{type(e)}: {e}")


def list_sharepoint_connections():
    fs_conns = list_filesystem_connections()
    sp_conns = [conn for conn in fs_conns if conn["type"] == FileSystemConnectionType.SHAREPOINT_ONLINE.value]
    return sp_conns


def list_filesystem_connections():
    filesystem_connections = []
    try:
        # Transform DSS connection data to our format, preserving the original mapping logic
        filesystem_connections = list_connections_from_type(FileSystemConnectionType.FILESYSTEM.value)
    except Exception as e:
        # Fallback to old implementation if new endpoint fails
        # See discussion in https://dataiku.slack.com/archives/CGD1MMU3Z/p1767950973675439?thread_ts=1767878778.156769&cid=CGD1MMU3Z
        logger.exception(
            f"Failed to get filesystem connections from new endpoint, falling back to old implementation: {e}"
        )
        connections_map = get_connections_map(FileSystemConnectionType)

        for conn_type in connections_map:
            for conn_name in connections_map[conn_type]:
                filesystem_connections.append({"id": conn_name, "label": conn_name, "type": conn_type})
    return filesystem_connections


def list_sql_connections():
    SQL_TYPE = "SQL"
    try:
        # Transform DSS connection data to our format, preserving the original mapping logic
        sql_connections = list_connections_from_type(SQL_TYPE)

    except Exception as e:
        # Fallback to the python API implementation if the first endpoint fails
        # See discussion in https://dataiku.slack.com/archives/CGD1MMU3Z/p1767950973675439?thread_ts=1767878778.156769&cid=CGD1MMU3Z
        logger.exception(
            f"Failed to get '{SQL_TYPE}' connections from new endpoint, falling back to old implementation: {e}"
        )
        connections_map = get_connections_map(SQLConnectionType)
        sql_connections = []
        for conn_type, conn_list in connections_map.items():
            for conn_name in conn_list:
                sql_connections.append({"id": conn_name, "label": f"{conn_name} ({conn_type})", "type": conn_type})
    return sql_connections


def list_agent_tools():
    from backend.utils.project_utils import list_project_agent_tools

    try:
        project = dataiku.api_client().get_default_project()
        tool_defs = []
        tools_objects = list_project_agent_tools(project)

        for tool in tools_objects:
            tool_type = tool.get("type")
            tool_name = tool.get("name")
            project_key = tool.get("projectKey")
            tool_id = tool.get("id")
            tool_description = tool.get("additionalDescriptionForLLM", "")
            tool_defs.append(
                {
                    "projectKey": project_key,
                    "id": tool_id,
                    "type": tool_type,
                    "name": tool_name,
                    "description": tool_description,
                }
            )

        return tool_defs
    except Exception as e:
        logger.error(f"Error listing agent tools: {e}")
        return []


def list_managed_folders():
    """
    Returns a list of managed folders in the current project.
    """
    try:
        client = dataiku.api_client()
        project = client.get_default_project()
        folders = []

        for folder in project.list_managed_folders():
            folder_id = folder.get("id")
            folder_name = folder.get("name", folder_id)
            folders.append({"id": folder_id, "name": folder_name})

        return folders
    except Exception as e:
        logger.error(f"Error listing managed folders: {e}")
        return []


@admin_bp.route("/config", methods=["GET"])
@log_http_request
def get_admin_config():
    """
    Get admin configuration from the admin_settings table.
    """
    config = read_config()
    payload_config = _normalize_config_response(config)
    return jsonify({"data": payload_config}), 200


@admin_bp.route("/config", methods=["PUT"])
@log_http_request
def put_admin_config():
    """
    Save admin configuration into the admin_settings table (global).
    """
    from backend.config import get_admin_uploads_managedfolder_id

    body = request.get_json(silent=True) or {}
    raw_config = body.get("config")
    socket_id = body.get("socketId")

    try:
        config = ADMIN_CONFIG_UPDATE_ADAPTER.validate_python(raw_config)
    except ValidationError as e:
        safe_errors = e.errors(include_url=False, include_context=False, include_input=False)
        logger.error(f"Admin config validation failed: {safe_errors}")
        return jsonify({"error": "Invalid request", "details": safe_errors}), 400

    embed_origin_errors = [
        *validate_embed_origins(
            config.get("allowedEmbedOrigins"),
            field_name="allowedEmbedOrigins",
        ),
        *validate_embed_path_prefixes(
            config.get("allowedEmbedOrigins"),
        ),
        *validate_embed_query_parameters(config.get("allowedEmbedOrigins")),
    ]
    if embed_origin_errors:
        logger.error("Admin config embed origin validation failed")
        return jsonify({"error": "Invalid request", "details": embed_origin_errors}), 400

    # `required` is server-controlled: never trust the client's submitted value, even if present.
    config["required"] = list(get_default_config_for_mode().get("required", []))

    current_config = read_config()

    keys_to_delete = []
    for key in LOOK_AND_FEEL_KEYS:
        if key in config and _is_empty_value(config[key]):
            config.pop(key)

    for key in ASSET_KEYS:
        old_path = _normalize_config_value(current_config.get(key))
        new_value = config.get(key) if key in config else None
        new_path = _normalize_config_value(new_value)
        if old_path and not new_path:
            keys_to_delete.append(key)

    # Delete asset files from managed folder (if folder is configured)
    if keys_to_delete:
        try:
            folder_id = get_admin_uploads_managedfolder_id()
            if not folder_id:
                logger.warning("No managed folder configured, skipping asset file deletion")
            else:
                client = dataiku.api_client()
                project = client.get_default_project()
                folder = project.get_managed_folder(folder_id)

                for key in keys_to_delete:
                    old_value = current_config.get(key)
                    asset_path = _normalize_config_value(old_value)

                    if asset_path:
                        try:
                            if hasattr(folder, "delete_path"):
                                folder.delete_path(asset_path)
                            elif hasattr(folder, "delete_file"):
                                folder.delete_file(asset_path)
                            logger.info(f"Deleted asset file at {asset_path} for key '{key}'")
                        except Exception as e:
                            logger.exception(f"Failed to delete asset file {asset_path} for key '{key}': {e}")
        except Exception as e:
            logger.error(f"Failed to delete asset files from managed folder: {e}")

    try:
        write_config(config)
    except PreconditionFailed as e:
        logger.exception(f"Error updating admin config: {e}")
        return jsonify({"error": str(e)}), 412
    except Exception as e:
        logger.exception(f"Error updating admin config: {e}")
        return jsonify({"error": str(e)}), 500

    fresh = read_config()
    payload_config = _normalize_config_response(fresh)

    # Notify other clients of config change
    try:
        socketio.emit("admin:config_updated", {"version": fresh.get("version")}, skip_sid=socket_id)
    except Exception as e:
        logger.error(f"Error emitting admin:config_updated event: {e}")

    return jsonify({"data": payload_config}), 200


@admin_bp.route("/enterprise/projects", methods=["GET"])
@log_http_request
def list_projects():
    try:
        projects = dataiku.api_client().list_projects()
        workspaces = list_workspaces()

        payload = {
            "projects": [
                {
                    "key": p.get("projectKey"),
                    "name": p.get("name"),
                }
                for p in projects
                if p.get("projectKey")
            ],
            "workspaces": [
                {
                    "id": ws.get("workspaceKey"),
                    "label": ws.get("displayName") or ws.get("workspaceKey"),
                    "description": ws.get("description", ""),
                }
                for ws in workspaces
            ],
        }
        return jsonify({"data": payload}), 200
    except Exception as e:
        logger.error(f"Error listing projects: {e}")
        return jsonify({"error": str(e)}), 500


@admin_bp.route("/enterprise/projects/<project_key>/agents", methods=["GET"])
@log_http_request
def list_project_agents(project_key: str):
    try:
        proj = dataiku.api_client().get_project(project_key)
        agents_list = []
        for agent in proj.list_agents():
            agent_dict = {"id": agent.get("id"), "name": agent.get("name"), "type": "agent"}

            # Extract pluginAgentType from the active version
            active_version = agent.get("activeVersion")
            versions = agent.get("versions", [])
            if active_version and versions:
                for version in versions:
                    if version.get("versionId") == active_version:
                        plugin_agent_type = version.get("pluginAgentType")
                        if plugin_agent_type:
                            agent_dict["pluginAgentType"] = plugin_agent_type
                        break

            agents_list.append(agent_dict)

        aug_llms = [
            {"id": llm.get("id"), "name": llm.get("friendlyName"), "type": "augmented_llm"}
            for llm in proj.list_llms()
            if llm.get("id").startswith("retrieval-augmented-llm")
        ]
        return (
            jsonify(
                {
                    "data": {
                        "project_key": project_key,
                        "agents": sorted(agents_list + aug_llms, key=lambda x: x["name"].lower()),
                    }
                }
            ),
            200,
        )
    except Exception as e:
        logger.error(f"Error listing agents for project {project_key}: {e}")
        return jsonify({"error": str(e)}), 500


@admin_bp.route("/myagents/options", methods=["GET"])
@log_http_request
def myagents_options():
    try:
        # Try to get connection options from DSS API endpoint using perform_http
        # This endpoint returns connection information including filesystem connections
        filesystem_connections = list_filesystem_connections()
        sql_connections = list_sql_connections()
        sharepoint_connections = list_sharepoint_connections()
        # Get real project folders
        folders = list_projects_folders()

        # Get real agent tools
        tools = list_agent_tools()

        data = {
            "fileSystemConnections": filesystem_connections,
            "SQLConnections": sql_connections,
            "sharePointConnections": sharepoint_connections,
            "folders": folders,
            "managedTools": tools,
        }

        return jsonify({"data": data}), 200
    except Exception as e:
        logger.error(f"Error getting myagents options: {e}")
        return jsonify({"error": str(e)}), 500


@admin_bp.route("/managed-folders", methods=["GET"])
@log_http_request
def get_managed_folders():
    """Get list of managed folders for documents uploads."""
    try:
        folders = list_managed_folders()
        return jsonify({"data": folders}), 200
    except Exception as e:
        logger.error(f"Error getting managed folders: {e}")
        return jsonify({"error": str(e)}), 500


TYPES_TO_EXCLUDE = {"SAVED_MODEL_AGENT", "RETRIEVAL_AUGMENTED"}


def list_llms_by_purpose(project, purpose):
    llms = []
    all_llms = project.list_llms(purpose=purpose)

    from backend.config import is_answers_mode_on

    for llm in all_llms:
        llm_type = llm.get("type")
        if llm_type in TYPES_TO_EXCLUDE and not (is_answers_mode_on()):
            continue

        llms.append({"id": llm.get("id"), "name": llm.get("friendlyName"), "type": llm_type})

    return llms


@admin_bp.route("/llms/available", methods=["GET"])
@log_http_request
def list_available_llms():
    try:
        client = dataiku.api_client()
        project = client.get_project(dataiku.default_project_key())

        text_models = list_llms_by_purpose(project, "GENERIC_COMPLETION")
        embedding_models = list_llms_by_purpose(project, "TEXT_EMBEDDING_EXTRACTION")

        data = {"textCompletionModels": text_models, "embeddingModels": embedding_models}

        return jsonify({"data": data}), 200

    except Exception as e:
        logger.exception("Erreur lors de la récupération des LLMs")
        return jsonify({"error": str(e)}), 500


@admin_bp.route("/upload", methods=["POST"])
@log_http_request
def admin_upload_files():
    """
    Upload admin files (white label assets, etc.) to managed folder.

    Form parameters:
    - files: The file(s) to upload
    - settingKey: The setting key this file belongs to
    - managedFolderId: The managed folder ID
    """
    try:
        user_id = g.get("authIdentifier", "unknown")

        setting_key = request.form.get("settingKey")
        if not setting_key:
            logger.error(f"Missing setting_key in admin upload request from user {user_id}")
            return jsonify({"error": "Missing required parameter: settingKey"}), 400

        folder_id = request.form.get("managedFolderId")
        if not folder_id:
            logger.error(f"Failed to access managed folder '{folder_id}' for user {user_id}")
            return (
                jsonify(
                    {
                        "error": f"Managed folder not found or inaccessible. Please verify you selected the managed folder in the settings"
                    }
                ),
                400,
            )

        if "files" not in request.files:
            logger.error(f"No files provided in admin upload request from user {user_id}")
            return jsonify({"error": "No files provided"}), 400

        files = request.files.getlist("files")
        if not files or all(f.filename == "" for f in files):
            logger.error(f"No files selected in admin upload request from user {user_id}")
            return jsonify({"error": "No files selected"}), 400

        if len(files) > 1:
            logger.error(f"Multiple files provided in admin upload request from user {user_id}")
            return jsonify({"error": "Only one file per setting key is allowed"}), 400

        client = dataiku.api_client()
        project = client.get_default_project()

        try:
            folder = project.get_managed_folder(folder_id)
        except Exception as e:
            logger.error(f"Failed to access managed folder '{folder_id}' for user {user_id}: {e}")
            return (
                jsonify(
                    {
                        "error": f"Managed folder not found or inaccessible. Please verify the folder ID '{folder_id}' exists and is accessible."
                    }
                ),
                500,
            )

        base_path = f"inputs/assets/{user_id}/{setting_key}"

        try:
            keep_path = f"{base_path}/.keep"
            folder.put_file(keep_path, b"")
        except Exception as e:
            logger.exception(f"Could not create .keep file: {e}")

        file = files[0]
        filename = file.filename

        asset_id = str(uuid.uuid4())
        unique_seed = f"{user_id}:{setting_key}:{filename}:{asset_id}".encode("utf-8")
        hash_prefix = hashlib.sha256(unique_seed).hexdigest()[:16]
        stored_filename = f"{hash_prefix}_{filename}"
        file_path = f"{base_path}/{stored_filename}"

        try:
            folder.put_file(file_path, file.stream)

            file.stream.seek(0, 2)
            file_size = file.stream.tell()
            file.stream.seek(0)

            logger.info(f"Uploaded admin asset: {filename} -> {file_path} for setting '{setting_key}'")
        except Exception as e:
            logger.error(f"Failed to upload {filename}: {e}")
            return jsonify({"error": f"Failed to upload {filename}: {str(e)}"}), 500

        asset_reference = {
            "assetId": asset_id,
            "path": file_path,
            "filename": filename,
            "storedFilename": stored_filename,
            "size": file_size,
            "type": file.content_type or "application/octet-stream",
            "uploadedAt": datetime.utcnow().isoformat(),
            "isDraft": True,
        }

        logger.info(f"Uploaded draft asset for user {user_id}, key '{setting_key}': {file_path}")
        return (
            jsonify(
                {
                    "success": True,
                    "settingKey": setting_key,
                    "asset": asset_reference,
                    "message": f"Successfully uploaded {filename}",
                }
            ),
            200,
        )

    except Exception as e:
        logger.error(f"Admin upload error: {e}")
        return jsonify({"error": f"Upload failed: {str(e)}"}), 500


@admin_bp.route("/assets/cleanup-drafts", methods=["POST"])
@log_http_request
def cleanup_draft_assets():
    """
    Clean up draft assets that weren't saved.

    Body parameters:
    - assetPaths: List of asset file paths to delete
    - managedFolderId: The managed folder ID
    """
    try:
        user_id = g.get("authIdentifier", "unknown")
        body = request.get_json(silent=True) or {}
        asset_paths = body.get("assetPaths", [])
        folder_id = body.get("managedFolderId")

        if not folder_id:
            logger.error(f"Missing managedFolderId in cleanup draft assets request from user {user_id}")
            return jsonify({"error": "Missing required parameter: managedFolderId"}), 400

        if not isinstance(asset_paths, list):
            logger.error(f"Invalid assetPaths in cleanup draft assets request from user {user_id}")
            return jsonify({"error": "assetPaths must be a list of file paths"}), 400

        client = dataiku.api_client()
        project = client.get_default_project()
        folder = project.get_managed_folder(folder_id)

        deleted_count = 0
        errors = []

        for asset_path in asset_paths:
            if not isinstance(asset_path, str) or not asset_path:
                continue

            try:
                if hasattr(folder, "delete_path"):
                    folder.delete_path(asset_path)
                elif hasattr(folder, "delete_file"):
                    folder.delete_file(asset_path)
                logger.info(f"Deleted draft asset: {asset_path}")
                deleted_count += 1
            except Exception as e:
                error_msg = f"Failed to delete {asset_path}: {str(e)}"
                logger.exception(error_msg)
                errors.append(error_msg)

        return (
            jsonify(
                {
                    "success": True,
                    "deletedCount": deleted_count,
                    "errors": errors if errors else None,
                    "message": f"Cleaned up {deleted_count} draft asset(s)",
                }
            ),
            200,
        )

    except Exception as e:
        logger.error(f"Error cleaning up draft assets: {e}")
        return jsonify({"error": f"Cleanup failed: {str(e)}"}), 500
