# SPDX-FileCopyrightText: 2025 Weibo, Inc.
#
# SPDX-License-Identifier: Apache-2.0

"""
Model resolver for Chat Shell.

Resolves model configuration from Bot's bound model or task-level override.
"""

import json
import logging
import os
import re
from typing import Any, Dict, Optional

from sqlalchemy.orm import Session

from app.core.config import settings
from app.models.kind import Kind
from app.schemas.kind import Bot, Model
from shared.models.execution import ExecutionRequest
from shared.utils.crypto import decrypt_api_key

logger = logging.getLogger(__name__)


def resolve_env_placeholder(value: str) -> str:
    """
    Resolve environment variable placeholders in a string.

    Supports format: ${ENV_VAR_NAME}

    Args:
        value: String that may contain environment variable placeholders

    Returns:
        String with placeholders replaced by environment variable values
    """
    if not value or not isinstance(value, str):
        return value

    # Pattern to match ${ENV_VAR_NAME}
    pattern = r"\$\{([^}]+)\}"

    def replace_env(match):
        env_var = match.group(1)
        env_value = os.environ.get(env_var)
        if env_value is not None:
            logger.info(
                f"[model_resolver] Resolved env var ${{{env_var}}} to value (length={len(env_value)})"
            )
            return env_value
        else:
            logger.warning(
                f"[model_resolver] Env var ${{{env_var}}} not found, keeping placeholder"
            )
            return match.group(0)  # Return original placeholder unchanged

    return re.sub(pattern, replace_env, value)


def resolve_value_from_source(
    data_sources: Dict[str, Dict[str, Any]], source_spec: str
) -> str:
    """
    Resolve value from specified data source using flexible notation.

    This is a port of the executor's config_utils.resolve_value_from_source function.

    Args:
        data_sources: Dictionary containing all available data sources
        source_spec: Source specification in format "source_name.path" or just "path"

    Returns:
        The resolved value or empty string if not found
    """
    try:
        # Parse source specification
        if "." in source_spec:
            # Format: "source_name.path"
            parts = source_spec.split(".", 1)
            source_name = parts[0]
            path = parts[1]
        else:
            # Format: just "path", use default source
            source_name = "agent_config"
            path = source_spec

        # Get the specified data source
        if source_name not in data_sources:
            return ""

        data = data_sources[source_name]

        # Navigate the path
        keys = path.split(".")
        current = data

        for key in keys:
            if isinstance(current, dict) and key in current:
                current = current[key]
            elif (
                isinstance(current, list) and key.isdigit() and int(key) < len(current)
            ):
                current = current[int(key)]
            else:
                return ""

        return str(current) if current is not None else ""
    except Exception:
        return ""


def replace_placeholders_with_sources(
    template: str, data_sources: Dict[str, Dict[str, Any]]
) -> str:
    """
    Replace placeholders in template with values from multiple data sources.

    This is a port of the executor's config_utils.replace_placeholders_with_sources function.

    Args:
        template: The template string with placeholders like ${agent_config.env.user} or ${env.user}
        data_sources: Dictionary containing all available data sources

    Returns:
        The template with placeholders replaced with actual values
    """
    # Find all placeholders in format ${source_spec}
    pattern = r"\$\{([^}]+)\}"

    def replace_match(match):
        source_spec = match.group(1)
        value = resolve_value_from_source(data_sources, source_spec)
        return value

    return re.sub(pattern, replace_match, template)


def get_default_headers_from_env() -> Dict[str, Any]:
    """
    Get DEFAULT_HEADERS from environment variable.

    This mirrors the executor's ConfigManager._parse_default_headers method.
    Reads from EXECUTOR_ENV or DEFAULT_HEADERS environment variable.

    Returns:
        Parsed default headers dictionary
    """
    default_headers = {}

    try:
        # First try EXECUTOR_ENV (JSON format)
        executor_env_str = os.environ.get("EXECUTOR_ENV", "")
        if executor_env_str:
            try:
                executor_env = json.loads(executor_env_str)
                if isinstance(executor_env, dict):
                    dh = executor_env.get("DEFAULT_HEADERS")
                    if dh:
                        if isinstance(dh, dict):
                            default_headers = dh
                        elif isinstance(dh, str):
                            default_headers = json.loads(dh)
                        logger.info(
                            f"[model_resolver] Loaded DEFAULT_HEADERS from EXECUTOR_ENV: keys={list(default_headers.keys())}"
                        )
            except json.JSONDecodeError as e:
                logger.warning(f"[model_resolver] Failed to parse EXECUTOR_ENV: {e}")

        # Fallback to DEFAULT_HEADERS env var
        if not default_headers:
            dh_str = os.environ.get("DEFAULT_HEADERS", "")
            if dh_str:
                try:
                    default_headers = json.loads(dh_str)
                    logger.info(
                        f"[model_resolver] Loaded DEFAULT_HEADERS from env: keys={list(default_headers.keys())}"
                    )
                except json.JSONDecodeError as e:
                    logger.warning(
                        f"[model_resolver] Failed to parse DEFAULT_HEADERS env: {e}"
                    )
    except Exception as e:
        logger.warning(f"[model_resolver] Failed to load DEFAULT_HEADERS from env: {e}")

    return default_headers


def build_default_headers_with_placeholders(
    default_headers: Dict[str, Any], data_sources: Dict[str, Dict[str, Any]]
) -> Dict[str, Any]:
    """
    Build default headers with placeholder replacement.

    This is a port of the executor's ConfigManager.build_default_headers_with_placeholders method.

    Args:
        default_headers: Raw default headers dictionary (may contain placeholders)
        data_sources: Dictionary containing all available data sources

    Returns:
        Default headers with placeholders replaced
    """
    result_headers = {}
    try:
        # Apply placeholder replacement on individual string values inside the dict
        for k, v in default_headers.items():
            if isinstance(v, str):
                result_headers[k] = replace_placeholders_with_sources(v, data_sources)
            else:
                result_headers[k] = v
        logger.info(
            f"Built default_headers with placeholders: keys={list(result_headers.keys())}"
        )
    except Exception as e:
        logger.warning(
            f"Failed to build default headers; proceeding without. Error: {e}"
        )
        result_headers = {}

    return result_headers


def _process_model_config_placeholders(
    model_config: Dict[str, Any],
    user_id: int,
    user_name: str,
    agent_config: Optional[Dict[str, Any]] = None,
    task_data: Optional[ExecutionRequest] = None,
) -> Dict[str, Any]:
    """
    Process placeholders in model_config (api_key and default_headers).

    This is an internal function used by extract_and_process_model_config.
    Handles placeholders like ${user.user_name}, ${agent_config.env.xxx}, etc.

    Args:
        model_config: Model configuration dict from _extract_model_config
        user_id: Current user's ID
        user_name: Current user's username
        agent_config: Optional agent config from bot (for chat mode)
        task_data: Optional[ExecutionRequest] containing task-specific data (for chat mode)

    Returns:
        Model config with placeholders replaced in api_key and default_headers
    """
    # Build user info for data sources
    user_info = {
        "id": user_id,
        "name": user_name or "",
        "user_name": user_name or "",
    }

    # Build task_data dict for data_sources
    # This ensures ${task_data.user.name} placeholders work even without full task context
    if task_data is not None:
        if isinstance(task_data, dict):
            effective_task_data = task_data
        elif hasattr(task_data, "to_dict") and callable(task_data.to_dict):
            effective_task_data = task_data.to_dict()
        else:
            effective_task_data = {}
    else:
        effective_task_data = {}

    if "user" not in effective_task_data:
        effective_task_data = {**effective_task_data, "user": user_info}

    # Build data_sources for placeholder replacement
    # This mirrors the chat.py logic for handling ${user.name}, ${task_data.user.name}, etc.
    data_sources = {
        "agent_config": agent_config or {},
        "model_config": model_config,
        "task_data": effective_task_data,
        "user": user_info,
        "env": model_config.get("default_headers", {}),
    }

    # Process api_key placeholder if it contains ${...} pattern
    api_key = model_config.get("api_key", "")
    if api_key and "${" in api_key:
        processed_api_key = replace_placeholders_with_sources(api_key, data_sources)
        if processed_api_key:
            model_config["api_key"] = processed_api_key
            logger.debug(
                f"[model_resolver] Processed api_key placeholder, from: {api_key} "
            )

    # Process DEFAULT_HEADERS with placeholder replacement
    raw_default_headers = model_config.get("default_headers", {})
    if raw_default_headers:
        processed_headers = build_default_headers_with_placeholders(
            raw_default_headers, data_sources
        )
        model_config["default_headers"] = processed_headers
        logger.info(f"[model_resolver] Processed default_headers with placeholders")

    return model_config


def extract_and_process_model_config(
    model_spec: Dict[str, Any],
    user_id: int,
    user_name: str,
    agent_config: Optional[Dict[str, Any]] = None,
    task_data: Optional[ExecutionRequest] = None,
) -> Dict[str, Any]:
    """
    Extract model configuration from spec and process all placeholders.

    This is the main public function that combines _extract_model_config
    and _process_model_config_placeholders into a single call.

    Used by both chat and wizard to get a fully processed model config.

    Args:
        model_spec: Model specification dictionary (from model.json.spec)
        user_id: Current user's ID
        user_name: Current user's username
        agent_config: Optional agent config from bot (for chat mode)
        task_data: Optional[ExecutionRequest] containing task-specific data
            used for placeholder substitution (e.g., ${task_data.user.name})

    Returns:
        Dict with fully processed model configuration:
        {
            "api_key": "sk-xxx",  # decrypted and placeholders resolved
            "base_url": "https://api.openai.com/v1",
            "model_id": "gpt-4",
            "model": "openai",
            "default_headers": {...}  # placeholders resolved
        }
    """
    # Step 1: Extract basic model config (handles env var placeholders and decryption)
    model_config = _extract_model_config(model_spec)

    # Step 2: Process data source placeholders (${user.xxx}, ${agent_config.xxx}, etc.)
    model_config = _process_model_config_placeholders(
        model_config=model_config,
        user_id=user_id,
        user_name=user_name,
        agent_config=agent_config,
        task_data=task_data,
    )

    return model_config


def _resolve_model_for_bot(
    db: Session,
    bot: Kind,
    user_id: int,
    override_model_name: Optional[str] = None,
    force_override: bool = False,
) -> tuple[Optional[Kind], Optional[Dict[str, Any]], Optional[str], Dict[str, Any]]:
    """
    Resolve model Kind and spec for a bot.

    Shared logic for build_agent_config_for_bot and get_model_config_for_bot.

    Resolution priority:
    1. override_model_name with force_override=True (task-level override)
    2. bot.spec.agent_config.bind_model (bot-level binding)
    3. bot.spec.modelRef (legacy reference)
    4. override_model_name without force_override (fallback)

    Args:
        db: Database session
        bot: The Bot Kind object
        user_id: User ID for querying user-specific models
        override_model_name: Optional model name to override
        force_override: If True, override_model_name takes highest priority

    Returns:
        Tuple of (model_kind, model_spec, model_name, raw_agent_config).
        model_kind/model_spec/model_name may be None if no model is found.

    Raises:
        ValueError: If override_model_name is not in the bot's allowed_models whitelist.
    """
    bot_crd = Bot.model_validate(bot.json)
    bot_json = bot.json or {}
    bot_spec = bot_json.get("spec", {})
    raw_agent_config = bot_spec.get("agent_config", {})

    model_name = None

    # Priority 1: Force override from task
    if force_override and override_model_name:
        model_name = override_model_name
        logger.info(f"Using task model (force override): {model_name}")
    else:
        # Priority 2: Bot's agent_config.bind_model
        bind_model = raw_agent_config.get("bind_model")
        if bind_model and isinstance(bind_model, str) and bind_model.strip():
            model_name = bind_model.strip()
            logger.info(f"Using bot bound model: {model_name}")

        # Priority 3: Bot's modelRef (legacy)
        if not model_name and bot_crd.spec.modelRef:
            model_name = bot_crd.spec.modelRef.name
            logger.info(f"Using bot modelRef: {model_name}")

        # Priority 4: Task-level override (fallback)
        if not model_name and override_model_name:
            model_name = override_model_name
            logger.info(f"Using task model (fallback): {model_name}")

    if not model_name:
        return None, None, None, raw_agent_config

    # Validate against allowed_models whitelist when an override model is used.
    # This is the single validation point covering all call paths (chat, task creation,
    # subscription, retry, etc.).
    allowed_models = raw_agent_config.get("allowed_models")
    if (
        allowed_models
        and override_model_name
        and (force_override or override_model_name)
    ):
        if isinstance(allowed_models, list) and len(allowed_models) > 0:
            allowed_names = {
                m.get("name")
                for m in allowed_models
                if isinstance(m, dict) and m.get("name")
            }
            if model_name not in allowed_names:
                raise ValueError(
                    f"Model '{model_name}' is not in the allowed models list for bot '{bot.name}'"
                )
            logger.info(
                f"Model '{model_name}' passed allowed_models whitelist check for bot '{bot.name}'"
            )

    # Find the model Kind object
    model_kind, model_spec = _find_model_with_namespace(db, model_name, user_id)
    return model_kind, model_spec, model_name, raw_agent_config


def build_agent_config_for_bot(
    db: Session,
    bot: Kind,
    user_id: int,
    override_model_name: Optional[str] = None,
    force_override: bool = False,
) -> Dict[str, Any]:
    """
    Build agent_config for a bot based on its model binding.

    Resolves the model and returns agent_config in the format that
    downstream executors expect: {"env": {"model": "...", "api_key": "...", ...}}.
    """
    # Short-circuit: if raw agent_config already has inline model config, use it
    bot_json = bot.json or {}
    raw_agent_config = bot_json.get("spec", {}).get("agent_config", {})
    if not (force_override and override_model_name):
        raw_env = raw_agent_config.get("env", {})
        if raw_env.get("model") and raw_env.get("api_key"):
            return raw_agent_config

    model_kind, model_spec, model_name, raw_agent_config = _resolve_model_for_bot(
        db, bot, user_id, override_model_name, force_override
    )

    if not model_spec:
        if model_name:
            logger.warning(
                "[build_agent_config_for_bot] Model '%s' not found, "
                "returning raw agent_config",
                model_name,
            )
        return raw_agent_config

    # Return the model's modelConfig directly - this contains the "env" dict
    # that executors expect (e.g., {"env": {"model": "claude", "api_key": "...", ...}})
    model_config_dict = model_spec.get("modelConfig", {})
    if not model_config_dict:
        return raw_agent_config

    # Include protocol if present in the model spec
    protocol = model_spec.get("protocol")
    if protocol:
        model_config_dict = dict(model_config_dict)
        model_config_dict["protocol"] = protocol

    return model_config_dict


def get_model_config_for_bot(
    db: Session,
    bot: Kind,
    user_id: int,
    override_model_name: Optional[str] = None,
    force_override: bool = False,
) -> Dict[str, Any]:
    """
    Get model configuration for a Bot (flat format for Chat Shell).

    Returns:
        Dict containing model configuration:
        {
            "api_key": "sk-xxx",
            "base_url": "https://api.openai.com/v1",
            "model_id": "gpt-4",
            "model": "openai",  # or "claude"
            "model_name": "my-model",  # Model CRD name for subscription
            "model_namespace": "default"  # Model CRD namespace for subscription
        }

    Raises:
        ValueError: If no model is configured or model not found
    """
    model_kind, model_spec, model_name, _ = _resolve_model_for_bot(
        db, bot, user_id, override_model_name, force_override
    )

    if not model_name:
        raise ValueError(f"Bot {bot.name} has no model configured")

    if not model_spec:
        raise ValueError(f"Model {model_name} not found")

    # Use the actual namespace from the found model
    model_namespace = (
        model_kind.namespace if model_kind and model_kind.namespace else "default"
    )

    # Extract configuration and add model_name/model_namespace
    config = _extract_model_config(model_spec)
    config["model_name"] = model_name
    config["model_namespace"] = model_namespace
    return config


def _find_model(db: Session, model_name: str, user_id: int) -> Optional[Dict[str, Any]]:
    """
    Find model by name.

    Search order:
    1. User's private models (kinds table)
    2. Group models (kinds table in user's groups)
    3. Public models (kinds table with user_id=0)

    Args:
        db: Database session
        model_name: Model name to find
        user_id: User ID for private model lookup

    Returns:
        Model spec dictionary or None if not found
    """
    model_kind, model_spec = _find_model_with_namespace(db, model_name, user_id)
    return model_spec


def _find_model_with_namespace(
    db: Session, model_name: str, user_id: int
) -> tuple[Optional[Kind], Optional[Dict[str, Any]]]:
    """
    Find model by name and return both the Kind object and spec.

    Search order:
    1. User's private models (kinds table)
    2. Group models (kinds table in user's groups)
    3. Public models (kinds table with user_id=0)

    Args:
        db: Database session
        model_name: Model name to find
        user_id: User ID for private model lookup

    Returns:
        Tuple of (Kind object, Model spec dictionary) or (None, None) if not found
    """
    # Search user's private models first
    user_model = (
        db.query(Kind)
        .filter(
            Kind.kind == "Model",
            Kind.user_id == user_id,
            Kind.name == model_name,
            Kind.is_active == True,
        )
        .first()
    )

    if user_model and user_model.json:
        logger.info(
            f"Found model '{model_name}' in user's private models (namespace: {user_model.namespace})"
        )
        return user_model, user_model.json.get("spec", {})

    # Search group models
    from app.services.group_permission import get_user_groups

    user_groups = get_user_groups(db, user_id)
    if user_groups:
        group_model = (
            db.query(Kind)
            .filter(
                Kind.kind == "Model",
                Kind.name == model_name,
                Kind.namespace.in_(user_groups),
                Kind.is_active == True,
            )
            .first()
        )

        if group_model and group_model.json:
            logger.info(
                f"Found model '{model_name}' in group models (namespace: {group_model.namespace})"
            )
            return group_model, group_model.json.get("spec", {})

    # Search public models
    public_model = (
        db.query(Kind)
        .filter(
            Kind.user_id == 0,
            Kind.kind == "Model",
            Kind.name == model_name,
            Kind.namespace == "default",
            Kind.is_active == True,
        )
        .first()
    )

    if public_model and public_model.json:
        logger.info(
            f"Found model '{model_name}' in public models (namespace: {public_model.namespace})"
        )
        return public_model, public_model.json.get("spec", {})

    logger.warning(f"Model '{model_name}' not found in any source")
    return None, None


def _extract_model_config(model_spec: Dict[str, Any]) -> Dict[str, Any]:
    """
    Extract API configuration from model spec.

    Args:
        model_spec: Model specification dictionary

    Returns:
        Dict with api_key, base_url, model_id, model type, and default_headers
    """
    logger.info(
        f"[model_resolver] _extract_model_config: model_spec keys = {list(model_spec.keys())}"
    )

    model_config = model_spec.get("modelConfig", {})
    logger.info(
        f"[model_resolver] _extract_model_config: modelConfig keys = {list(model_config.keys()) if model_config else 'empty'}"
    )

    env = model_config.get("env", {})
    logger.info(
        f"[model_resolver] _extract_model_config: env keys = {list(env.keys()) if env else 'empty'}"
    )

    # Get raw values with defaults
    api_key = env.get("api_key", "")
    base_url = env.get("base_url", "https://api.openai.com/v1")
    model_id = env.get("model_id", "gpt-4")
    model_type = env.get("model", "openai")

    # Resolve environment variable placeholders in api_key
    if api_key and "${" in api_key:
        logger.info(
            f"[model_resolver] api_key contains placeholder, resolving from env..."
        )
        api_key = resolve_env_placeholder(api_key)

    # Get DEFAULT_HEADERS - first from modelConfig, then from env, then from environment variable
    default_headers = model_config.get("DEFAULT_HEADERS", {})
    if not default_headers:
        # Also check env for backward compatibility
        default_headers = env.get("DEFAULT_HEADERS", {})

    if not default_headers:
        # settings.EXECUTOR_ENV is a JSON string, need to parse it first
        try:
            executor_env = (
                json.loads(settings.EXECUTOR_ENV) if settings.EXECUTOR_ENV else {}
            )
            default_headers = executor_env.get("DEFAULT_HEADERS", {})
        except (json.JSONDecodeError, TypeError):
            logger.warning(
                f"[model_resolver] Failed to parse settings.EXECUTOR_ENV as JSON"
            )
            default_headers = {}

    # If still empty, try to get from environment variable (EXECUTOR_ENV or DEFAULT_HEADERS)
    if not default_headers:
        default_headers = get_default_headers_from_env()

    # If default_headers is a string, try to parse it as JSON
    if isinstance(default_headers, str):
        try:
            default_headers = json.loads(default_headers)
        except Exception:
            logger.warning(
                f"Failed to parse DEFAULT_HEADERS as JSON: {default_headers}"
            )
            default_headers = {}

    logger.info(
        f"[model_resolver] _extract_model_config: DEFAULT_HEADERS keys = {list(default_headers.keys()) if default_headers else 'empty'}"
    )

    # Log extracted values (mask API key)
    masked_key = (
        f"{api_key[:8]}...{api_key[-4:]}"
        if api_key and len(api_key) > 12
        else ("***" if api_key else "EMPTY")
    )
    logger.info(
        f"[model_resolver] _extract_model_config: api_key={masked_key}, base_url={base_url}, model_id={model_id}, model_type={model_type}"
    )

    # Decrypt API key if encrypted (only if it doesn't look like a placeholder)
    if api_key and "${" not in api_key:
        try:
            decrypted_key = decrypt_api_key(api_key)
            masked_decrypted = (
                f"{decrypted_key[:8]}...{decrypted_key[-4:]}"
                if decrypted_key and len(decrypted_key) > 12
                else ("***" if decrypted_key else "EMPTY")
            )
            logger.info(
                f"[model_resolver] _extract_model_config: decrypted api_key={masked_decrypted}"
            )
            api_key = decrypted_key
        except Exception as e:
            logger.warning(f"Failed to decrypt API key, using as-is: {e}")

    # Extract API format (for OpenAI-compatible models)
    # Priority: 1. apiFormat field, 2. protocol field (openai-responses)
    # Default to None for backward compatibility (will use chat/completions)
    api_format = model_spec.get("apiFormat")
    protocol = model_spec.get("protocol")

    # If protocol is "openai-responses", use responses API format
    if not api_format and protocol == "openai-responses":
        api_format = "responses"
        logger.info(
            f"[model_resolver] _extract_model_config: using responses API from protocol={protocol}"
        )
    # Context window and output token limits from modelConfig
    context_window = model_config.get("context_window")
    max_output_tokens = model_config.get("max_output_tokens")

    # Model category type (llm, video, tts, etc.) - used for routing
    model_category_type = model_spec.get("modelType")
    if model_category_type:
        logger.info(
            f"[model_resolver] _extract_model_config: modelType={model_category_type}"
        )

    # Video generation config (when modelType='video')
    video_config = model_spec.get("videoConfig")

    # Thinking/reasoning config (provider-native passthrough)
    # Only read from env - this is the single source of truth
    thinking_config = env.get("thinking_config") or env.get("thinkingConfig")
    if thinking_config:
        logger.info(
            f"[model_resolver] _extract_model_config: thinking_config={list(thinking_config.keys())}"
        )

    # Temperature override from env (user-configured, overrides default)
    temperature = env.get("temperature")
    if temperature is not None:
        logger.info(
            f"[model_resolver] _extract_model_config: temperature={temperature}"
        )

    return {
        "api_key": api_key,
        "base_url": base_url,
        "model_id": model_id,
        "model": model_type,
        "default_headers": default_headers,
        "api_format": api_format,
        # Protocol for special API routing (e.g., gemini-deep-research)
        "protocol": protocol,
        # Context window and output token limits from ModelSpec or modelConfig
        "context_window": context_window,
        "max_output_tokens": max_output_tokens,
        # Model category type for routing (llm, video, tts, etc.)
        "modelType": model_category_type,
        # Video generation config (resolution, ratio, duration, etc.)
        "videoConfig": video_config,
        # Thinking/reasoning config (provider-native passthrough)
        "think_config": thinking_config,
        # User-configured temperature override
        "temperature": temperature,
    }


def get_bot_system_prompt(
    db: Session, bot: Kind, user_id: int, team_member_prompt: Optional[str] = None
) -> str:
    """
    Get the system prompt for a Bot.

    Combines Ghost's system prompt with team member's additional prompt.
    The combined prompt is wrapped in <base_prompt> XML tags.

    Args:
        db: Database session
        bot: The Bot Kind object
        user_id: User ID (for Ghost lookup)
        team_member_prompt: Optional additional prompt from team member config

    Returns:
        Combined system prompt string wrapped in <base_prompt> tags
    """
    from app.schemas.kind import Ghost

    bot_crd = Bot.model_validate(bot.json)
    system_prompt = ""

    # Get Ghost for system prompt
    ghost = (
        db.query(Kind)
        .filter(
            Kind.user_id == user_id,
            Kind.kind == "Ghost",
            Kind.name == bot_crd.spec.ghostRef.name,
            Kind.namespace == bot_crd.spec.ghostRef.namespace,
            Kind.is_active == True,
        )
        .first()
    )

    if ghost and ghost.json:
        ghost_crd = Ghost.model_validate(ghost.json)
        system_prompt = ghost_crd.spec.systemPrompt or ""

    # Append team member prompt if provided
    if team_member_prompt:
        if system_prompt:
            system_prompt = f"{system_prompt}\n\n{team_member_prompt}"
        else:
            system_prompt = team_member_prompt

    # Wrap in <base_prompt> tags for structured prompt identification
    if system_prompt:
        system_prompt = f"<base_prompt>\n{system_prompt}\n</base_prompt>"

    return system_prompt
