import functools
import os
from datetime import datetime
from typing import Dict, List, Optional, TypedDict

import dataiku
from dataiku.customwebapp import get_webapp_config
from dataikuapi.dss.project import DSSProject
from flask import g
from langchain_core.tools import StructuredTool

from backend.cache import cache  # noqa: E402
from backend.constants import TEXT_EMBEDDING
from backend.utils.agents_utils import filter_agents_per_user, map_aug_llms_id_name
from backend.utils.logging_utils import get_logger

logger = get_logger(__name__)

LLM_ID = "openai:bs-openai:gpt-4o"
DB_FOLDER_PATH = ""
DEFAULT_CONFIG = {
    "LLMs": [
        {"$$hashKey": "object:287", "llm_id": LLM_ID},
    ],
    "tool_agent_configurations": [],
    "enable_agents_as_tools": False,
    "logLevel": "INFO",
    "projects_keys": [],
    "agents_ids": [],
    "tools": [],
    "db_folder_path": DB_FOLDER_PATH,
    "default_fs_connection": "filesystem_managed",
}


class AgentDetails(TypedDict):
    agent_id: str
    name: str
    agent_system_instructions: Optional[str]
    tool_agent_description: Optional[str]
    stories_workspace: Optional[str]
    agent_short_example_queries: Optional[List[str]]
    agent_example_queries: Optional[List[str]]


class ToolDetails(TypedDict):
    dss_id: str
    name: str
    description: Optional[str]
    type: Optional[str]
    lc_tool: StructuredTool


def is_local_dev():
    return os.getenv("LOCAL_DEV", "false").lower() == "true"


@cache.memoize()
def get_project_key():
    if is_local_dev():
        return os.getenv("DKU_CURRENT_PROJECT_KEY")
    else:
        return dataiku.default_project_key()


def update(original, updates):
    # Update the default config with local config
    for key, value in updates.items():
        if isinstance(original.get(key), list) and isinstance(value, list):
            original[key] = value
        elif isinstance(original.get(key), dict) and isinstance(value, dict):
            original[key].update(value)
        else:
            original[key] = value
    return original


def load_local_config():
    import json
    import locale
    from pathlib import Path

    local_config_path = Path(__file__).parent / "local_config.json"
    config = DEFAULT_CONFIG.copy()
    # Override with local configuration if it exists
    if local_config_path.exists():
        with open(local_config_path, "r", encoding=locale.getpreferredencoding(False)) as local_config_file:
            local_config = json.load(local_config_file)
            # Update the default config with local config
            update(config, local_config)
            # config.update(local_config)
    else:
        print(
            "No local configuration found. Default configuration will be used. Create a local_config.json file to override it."
        )
    return config


@cache.memoize()
def get_config():
    if is_local_dev():
        conf = load_local_config()
        print(f"Using CONF: {conf}")
        return conf
    else:
        return get_webapp_config()


def get_workload_folder_path():
    if is_local_dev():
        return load_local_config()["db_folder_path"]
    else:
        from dataiku.core import workload_local_folder  # DSS14.1

        return workload_local_folder.get_workload_local_folder_path()


def get_project_agents(current_project: DSSProject) -> List[Dict[str, str]]:
    agents_map = get_project_agent_mapping(current_project)
    return [
        {
            "value": current_project.project_key + ":" + agent_id,
            "label": agent_name,
            "description": "",
        }
        for agent_id, agent_name in agents_map.items()
    ]


def get_project_agent_mapping(current_project: DSSProject):
    return {
        llm.get("id"): llm.get("friendlyName")
        for llm in current_project.list_llms()
        if llm.get("id").startswith("agent")
    }


def list_agents_by_project(selected_projects) -> Dict[str, List[Dict[str, str]]]:
    client = dataiku.api_client()
    agents_by_project: Dict[str, List[Dict[str, str]]] = {}
    if not selected_projects:
        return agents_by_project
    for project_key in selected_projects:
        project_ob: DSSProject = client.get_project(project_key)
        agents = get_project_agents(project_ob)
        if agents:
            agents_by_project[project_key] = agents
    return agents_by_project


def map_agents_id_name(selected_projects, with_project_key: bool = False) -> Dict[str, str]:
    agents_by_project = list_agents_by_project(selected_projects)
    agents_map = {}
    for project_key, agents in agents_by_project.items():
        for agent in agents:
            agents_map[agent["value"]] = agent["label"] if not with_project_key else f"[{project_key}] {agent['label']}"
    return agents_map


def get_enterprise_agent_last_modified(
    project_id: str, object_id: str, is_augmented_llm: bool
) -> tuple[str | None, str | None]:
    project = dataiku.api_client().get_project(project_id)

    obj = project.get_retrieval_augmented_llm(object_id) if is_augmented_llm else project.get_agent(object_id)

    raw = obj.get_settings().get_raw()

    active = raw.get("activeVersion")
    versions_by_id = {v["versionId"]: v for v in raw.get("versions", [])}

    v = versions_by_id.get(active)

    last_modified_by, last_modified_on = None, None

    if v:
        tag = v.get("versionTag") or v.get("creationTag")
        if tag:
            last_modified_by = ((tag.get("lastModifiedBy") or {}).get("login")) or None
            try:
                ts = tag["lastModifiedOn"] / 1000.0
                last_modified_on = datetime.fromtimestamp(ts).strftime("%Y-%m-%dT%H:%M:%S.%f")
            except Exception:
                last_modified_on = None
    return last_modified_by, last_modified_on


def get_enterprise_agents(user: str):
    config = get_config()
    selected_projects = config.get("projects_keys")
    # selected agents and tool agent configuration will not be independant anymore; selected agents will be omitted so that
    # enterprise agents are the only field that add the selected agent
    tool_agent_configurations = config.get("tool_agent_configurations")
    selected_agents = [
        agent_config["agent_id"] for agent_config in tool_agent_configurations if "agent_id" in agent_config
    ]
    selected_aug_llms = config.get("augmented_llms_ids")
    aug_llms_confs = config.get("augmented_llms_configurations")
    available_agents: List[AgentDetails] = []
    user_accessible_agents = filter_agents_per_user(user, selected_agents) if selected_agents else []
    if user_accessible_agents and tool_agent_configurations:
        selected_agents_map = {
            agent["agent_id"]: [
                agent["agent_system_instructions"],
                agent["tool_agent_description"],
                agent.get("stories_workspace", "") if agent.get("enable_stories") else None,
                agent.get("agent_short_example_queries", []),
                agent.get("agent_example_queries", []),
                agent.get("tool_agent_display_name", ""),
            ]
            for agent in tool_agent_configurations
        }

        agents_ids_names = map_agents_id_name(selected_projects)
        available_agents = [
            {
                "id": id,
                "name": selected_agents_map.get(id)[5] if selected_agents_map.get(id)[5] else agents_ids_names[id],
                "agent_system_instructions": selected_agents_map.get(id)[0],
                "tool_agent_description": selected_agents_map.get(id)[1],
                "stories_workspace": selected_agents_map.get(id)[2],
                "agent_short_example_queries": selected_agents_map.get(id)[3],
                "agent_example_queries": selected_agents_map.get(id)[4],
            }
            for id in user_accessible_agents
            if id in selected_agents_map
        ]
    # Handle ra models the same way
    user_accessible_aug_llms = filter_agents_per_user(user, selected_aug_llms) if selected_aug_llms else []
    if user_accessible_aug_llms and aug_llms_confs:
        selected_agents_map = {
            agent["augmented_llm_id"]: [
                agent["augmented_llm_description"],
                agent.get("augllm_short_example_queries", []),
                agent.get("augllm_example_queries", []),
                agent.get("augmented_llm_display_name"),
            ]
            for agent in aug_llms_confs
        }
        agents_ids_names = map_aug_llms_id_name(selected_projects)
        if not available_agents:
            available_agents = []
        available_agents.extend(
            [
                {
                    "id": id,
                    "name": selected_agents_map.get(id)[3] if selected_agents_map.get(id)[3] else agents_ids_names[id],
                    "agent_system_instructions": "",
                    "stories_workspace": None,
                    "tool_agent_description": selected_agents_map.get(id)[0],
                    "agent_short_example_queries": selected_agents_map.get(id)[1],
                    "agent_example_queries": selected_agents_map.get(id)[2],
                }
                for id in user_accessible_aug_llms
                if id in selected_agents_map
            ]
        )

    return available_agents


@cache.memoize()
def agents_as_tools() -> bool:
    """
    Return True if we should run the meta‐query to preselect agents.
    """
    cfg_flag = get_config().get("enable_agents_as_tools", False)
    return bool(cfg_flag)


# TODO : is this still need cache ?
# @functools.lru_cache(maxsize=1)
def get_tools_details(user: str) -> list[ToolDetails]:
    """
    Fast helper used everywhere else.
    • Reads the list of *configured* tool ids
    • Delegates to the @lru_cache’d builder.
    """
    # TODO user will be used in impersonation
    if not user:
        raise ValueError("A user must be provided to get tool details.")

    ids = tuple(sorted(get_config().get("tools", [])))
    if not ids:  # nothing selected
        return []

    ids_set = set(ids)

    if not ids_set:
        return []
    logger.info(f"Building structured tool for {user}")
    project = dataiku.api_client().get_default_project()
    tool_details = []

    def extract_error_message(error_str: str) -> str:
        """Extract the meaningful message from error string, removing package names."""
        # Split by ':' and take the last non-empty part
        parts = [part.strip() for part in error_str.split(":") if part.strip()]
        if parts:
            # Return the last part which usually contains the actual message
            return parts[-1]
        return error_str

    for tool_def in project.list_agent_tools():
        tool_id = tool_def["id"]
        tool_type = None
        tool_name = None
        tool_description = None
        if tool_id not in ids_set:
            continue

        try:
            raw_settings = project.get_agent_tool(tool_id).get_settings().get_raw()
            tool_type = raw_settings.get("type")
            tool_name = raw_settings.get("name")
            tool_description = raw_settings.get("additionalDescriptionForLLM")

            tool_details.append(
                {
                    "dss_id": tool_id,
                    "name": tool_name,
                    "description": tool_description,
                    "type": tool_type,
                    "configured": True,  # Tool is properly configured
                }
            )
            logger.debug(f"tool_details {tool_id} ({tool_details})")

        except Exception as e:
            clean_error_msg = extract_error_message(str(e))
            logger.warning(f"Tool {tool_id} ({tool_def['name']}) is not properly configured: {clean_error_msg}")
            tool_details.append(
                {
                    "dss_id": tool_id,
                    "name": tool_def["name"],  # from metadata
                    "description": "Tool configuration incomplete",  # fallback description
                    "type": None,
                    "configured": False,  # Tool is not configured
                    "error": clean_error_msg,
                }
            )
    return list(tool_details)


# -------------------------------------------------------------------- #
# LLM helpers
# -------------------------------------------------------------------- #
def get_llms(purpose="GENERIC_COMPLETION") -> list[dict]:
    """
    Returns [{'id': 'openai:…', 'name': 'GPT-4o Mini'}, …]
    UI will render `name`, backend keeps using `id`.
    """
    client = dataiku.api_client()
    project = client.get_default_project()
    id_to_name = {llm.id: llm.get("friendlyName") for llm in project.list_llms(purpose)}
    out = []

    llms = []
    if purpose == TEXT_EMBEDDING:
        lembeding_llm = get_config().get("embedding_llm")
        if lembeding_llm:
            llms = [{"llm_id": lembeding_llm}]
    else:
        llms = get_config().get("LLMs", [])

    for entry in llms:
        llm_id = entry.get("llm_id")
        if llm_id:
            name = id_to_name.get(llm_id, entry.get("name", llm_id))
            out.append({"id": llm_id, "name": name})
    return out


def get_default_llm_id() -> str:
    if get_config().get("default_llm_id"):
        return get_config().get("default_llm_id")
    else:
        raise ValueError("No base LLM configured")


def get_charts_generation_llm_id() -> str:
    if get_config().get("charts_generation_llm_id"):
        return get_config().get("charts_generation_llm_id")
    else:
        return get_default_llm_id()


def get_default_embedding_llm() -> str:
    return get_config().get("embedding_llm")


@cache.memoize()
def get_global_system_prompt() -> str:
    """
    Single source of truth for the application-level system prompt.
    ConversationService will add it *only* when:
      • zero agent or
      • multi-agent orchestration.
    """
    return get_config().get("globalSystemPrompt", "You are a helpful and friendly assistant.")


def get_ui_config():
    config = get_config()
    return {
        "visualization_mode": config.get("visualization_generation_mode"),
        "enable_quick_agents": config.get("enable_quick_agents", True),
        "enable_prompt_library": config.get("enable_prompt_library", True),
        "enable_app_smart_mode": config.get("enable_app_smart_mode", True),
        "default_llm": get_default_llm_id(),
        "homepage_examples": config.get("homepage_examples", []),
    }
