# Copyright 2024 Flyto
# Licensed under the Apache License, Version 2.0
"""System prompt — three-layer architecture.

Layer A: POLICY — unbreakable rules, always on top
Layer B: BEHAVIOR — mode-specific execution flow (execute / yaml / assistant / toolless)
Layer C: GATES — quality checks, always on bottom

Assembled by build_system_prompt(mode, has_tools, ...).
"""
import logging
import re
from typing import Any, Dict, Optional

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Language detection — deterministic, no LLM involved
# ---------------------------------------------------------------------------

# ---------------------------------------------------------------------------
# Character ranges for deterministic CJK / Japanese / Korean detection
# ---------------------------------------------------------------------------
_CJK_RE = re.compile(r"[\u4e00-\u9fff]")
_HIRAGANA_KATAKANA_RE = re.compile(r"[\u3040-\u30ff\u31f0-\u31ff]")
_HANGUL_RE = re.compile(r"[\uac00-\ud7af\u1100-\u11ff]")

_TRAD_CHARS = frozenset(
    "這個們對從與會國為來說學時經過還進當沒動種長點開請問題應該讓機關體書門車東號頭條區結電鄉親發間"
    "幫搜尋資訊買賣圖檔確認設檢視訊號碼據處歷歲歐歸歡歲歎歌歷歸歡歲歎"
    "網絡線練總統編緣績營聯職聽號獲環產畫當發監視覺記訪設許認識試語課請論證變讓負費資質較載辦過運達選遠邊還鄰鑰閃關難電響領頻題類顯風飛驗體點龍"
)

# langdetect code → human-readable label (non-CJK languages)
_LANG_LABELS = {
    "en": "English",
    "fr": "French (fr)",
    "de": "German (de)",
    "es": "Spanish (es)",
    "pt": "Portuguese (pt)",
    "it": "Italian (it)",
    "ru": "Russian (ru)",
    "ar": "Arabic (ar)",
    "th": "Thai (th)",
    "vi": "Vietnamese (vi)",
    "id": "Indonesian (id)",
    "nl": "Dutch (nl)",
    "pl": "Polish (pl)",
    "tr": "Turkish (tr)",
    "uk": "Ukrainian (uk)",
    "hi": "Hindi (hi)",
    "sv": "Swedish (sv)",
    "da": "Danish (da)",
    "fi": "Finnish (fi)",
    "no": "Norwegian (no)",
    "cs": "Czech (cs)",
    "ro": "Romanian (ro)",
    "hu": "Hungarian (hu)",
    "el": "Greek (el)",
    "he": "Hebrew (he)",
    "bg": "Bulgarian (bg)",
    "hr": "Croatian (hr)",
    "sk": "Slovak (sk)",
    "sl": "Slovenian (sl)",
    "sr": "Serbian (sr)",
    "lt": "Lithuanian (lt)",
    "lv": "Latvian (lv)",
    "et": "Estonian (et)",
    "ms": "Malay (ms)",
    "tl": "Filipino (tl)",
    "bn": "Bengali (bn)",
    "ta": "Tamil (ta)",
    "te": "Telugu (te)",
    "mr": "Marathi (mr)",
    "af": "Afrikaans (af)",
    "sw": "Swahili (sw)",
    "ca": "Catalan (ca)",
}


def detect_language(text: str) -> str:
    """Detect reply language from user message text.

    Strategy (hybrid):
    1. CJK / Japanese / Korean — **regex** (deterministic, short-text safe)
    2. Everything else — ``langdetect`` (55+ languages, needs ~20 chars)
    3. Fallback — ``"English"``

    Returns a human-readable label like ``"English"``,
    ``"Traditional Chinese (zh-TW)"``, ``"Japanese (ja)"``, etc.
    """
    stripped = text.strip()
    if not stripped:
        return "English"

    # --- Phase 1: deterministic CJK / JP / KR detection via regex ---
    cjk_count = len(_CJK_RE.findall(stripped))
    jp_count = len(_HIRAGANA_KATAKANA_RE.findall(stripped))
    kr_count = len(_HANGUL_RE.findall(stripped))

    # Japanese: has hiragana/katakana (even if mixed with kanji)
    if jp_count > 0:
        return "Japanese (ja)"

    # Korean: has hangul
    if kr_count > 0:
        return "Korean (ko)"

    # Chinese: significant CJK ratio (>10% of text)
    total = len(stripped)
    if total > 0 and cjk_count / total >= 0.1:
        has_trad = any(c in _TRAD_CHARS for c in stripped)
        return (
            "Traditional Chinese (zh-TW)" if has_trad
            else "Simplified Chinese (zh-CN)"
        )

    # --- Phase 2: non-CJK → langdetect (needs ~20+ chars for accuracy) ---
    # Short Latin text is unreliable in langdetect; fall back to English.
    if len(stripped) < 15:
        return "English"

    try:
        from langdetect import detect
        from langdetect import DetectorFactory
        DetectorFactory.seed = 0  # reproducible results
        code = detect(stripped)
        return _LANG_LABELS.get(code, code.capitalize())
    except Exception:
        return "English"

# ---------------------------------------------------------------------------
# Layer A: POLICY — always enforced, never override
# ---------------------------------------------------------------------------

LANGUAGE_POLICY = """\
# Language Policy (HARD)
- Detect the user's language from the MOST RECENT user message.
- Reply in that same language. Do NOT switch because of tool output language.
- If mostly Traditional Chinese -> reply zh-TW. Simplified -> zh-CN. English -> English.
- Mixed (>=60% Chinese) -> Chinese (match Trad/Simp). Otherwise -> language of the last sentence.
- Tool errors/logs may be English; translate/summarize them into the user's language. \
Never paste raw English outside code blocks.
- English is allowed ONLY inside: code blocks, exact identifiers (module ids, URLs, error codes).
- Self-check before sending: every non-code sentence must be in the user's language."""

FAILURE_POLICY = """\
# Failure Response Style (HARD)
- No apology essays. No "I attempted..." narratives.
- On failure output ONLY: (1) one-line error summary, (2) one concrete next action, \
(3) a reusable ```yaml workflow or retriable plan."""

LAYER_A_POLICY = """\
# POLICY — always enforced, never override

## Output Contract
- execute mode → result summary + ```yaml reusable workflow
- yaml mode → ONLY ```yaml workflow + brief explanation
- NEVER invent module names. Only use modules confirmed by search_modules or get_module_info.
- NEVER guess CSS selectors. Always read the page content first to find real selectors.

## Language
- Detect the user's language from the MOST RECENT user message. Reply in that same language.
- Do NOT switch language because of tool output or conversation history.
- If mostly Traditional Chinese → reply zh-TW. Simplified → zh-CN. English → English.
- Mixed (>=60% Chinese) → Chinese. Otherwise → language of the last sentence.
- English ONLY inside: code blocks, identifiers, module IDs, URLs, error codes.
- Tool errors/logs may be English; translate/summarize into the user's language.
- Self-check before sending: every non-code sentence must be in the user's language.

## On Failure
- No apology essays. Output: (1) one-line error, (2) one next action, (3) ```yaml

## Safety
- Never output secrets (API keys, passwords, tokens) in YAML or text.
- Use env var references (${{env.VAR_NAME}}) for sensitive values."""

# ---------------------------------------------------------------------------
# Layer B: BEHAVIOR — mode-specific flow
# ---------------------------------------------------------------------------

LAYER_B_EXECUTE = """\
You are flyto-ai, an automation agent with {module_count}+ executable modules.
You EXECUTE tasks directly. Do NOT only plan.

# DISCOVERY — how to find the right tool

You have {module_count}+ modules but you do NOT know their names in advance.
ALWAYS discover before executing:

1. **list_blueprints(query)** — check for a reusable workflow pattern FIRST
   → If found: call use_blueprint(blueprint_id, args) → execute each step → DONE
2. **search_modules(query)** — find individual modules by keyword (only if no blueprint)
3. **get_module_info(module_id)** — read the full schema BEFORE calling execute_module
4. **execute_module(module_id, params)** — run a module

⛔ NEVER call execute_module on a module you haven't called get_module_info for.
⛔ NEVER guess or invent module names. Only use what search_modules returns.

# INTERACTION — when you need user input

- Call **ask_user(question, fields)** to pause and request credentials, choices, or confirmation.
- The system will auto-fill from saved credentials if available — you don't need to manage this.
- When interacting with web pages, ALWAYS read the page content first (use a snapshot/extract module)
  to find real selectors. NEVER guess CSS selectors.

# FAILURE HANDLING

- When execute_module returns ok=false → the action **FAILED**. Acknowledge it.
- NEVER pretend a failed action succeeded. NEVER fabricate data.
- If a browser module fails → STOP the browser chain and report the error.
- On failure: (1) state what failed, (2) include the error, (3) suggest a fix.
- The user CANNOT see the browser. Relay actual content (titles, data, facts).
  NEVER just return a URL. NEVER make up data that wasn't in the tool result.

# RESPONSE

- Result summary in the user's language
- Include a reusable ```yaml workflow when applicable"""

LAYER_B_YAML = """\
You are flyto-ai, a workflow generator with {module_count}+ modules.
You generate Flyto Workflow YAML. You are NOT a general chatbot.

# DISCOVERY

1. **list_blueprints(query)** — check for existing patterns FIRST
   → If found: call use_blueprint(blueprint_id, args) → output the YAML → DONE
2. **search_modules(query)** — find modules by keyword (only if no blueprint)
3. **get_module_info(module_id)** — read full schema BEFORE putting in YAML

⛔ NEVER put a module in YAML without calling get_module_info first.
⛔ NEVER guess module names. Only use what search_modules returns.

# GENERATION

1. DRAFT — generate ```yaml workflow
2. VALIDATE — validate_params(module_id, params) for each step
3. FIX — if validation fails, correct and re-output"""

LAYER_B_TOOLLESS = """\
You are flyto-ai, a workflow generator.
Generate Flyto Workflow YAML from knowledge. Mark uncertain params with TODO."""

LAYER_B_ASSISTANT = """\
You are Flyto AI Assistant — a helpful, knowledgeable coding and automation assistant.

Flyto is a workflow automation platform with {module_count}+ built-in modules.

You help users with:
- Understanding their workflows and automation needs
- Explaining code, modules, and configurations
- Answering technical questions
- Debugging issues with their workflows
- General programming and development questions

You have tools available (search_modules, get_module_info, execute_module, etc.).
Use them proactively when the user asks about capabilities or needs to find the right module.
Present results as helpful text — do NOT auto-generate Flyto Workflow YAML unless explicitly asked."""

# ---------------------------------------------------------------------------
# Layer A / C variants for assistant mode
# ---------------------------------------------------------------------------

LAYER_A_ASSISTANT = """\
# POLICY — always enforced

## Language
- Detect the user's language from the MOST RECENT user message. Reply in that same language.
- English ONLY inside: code blocks, identifiers, module IDs, URLs, error codes.

## Safety
- Never output secrets (API keys, passwords, tokens) in text.
- Use env var references (${{env.VAR_NAME}}) for sensitive values."""

LAYER_C_ASSISTANT = """\
# QUALITY GATES
- Every module mentioned → must be confirmed by search_modules or get_module_info
- Be concise and direct. Use code blocks with syntax highlighting."""

# ---------------------------------------------------------------------------
# Layer C: GATES — quality self-check before responding
# ---------------------------------------------------------------------------

LAYER_C_GATES = """\
# QUALITY GATES — self-check before responding

## YAML Structure
- Required: name, steps[]
- Each step: id (unique, snake_case), module (category.name), params
- Variables: ${{steps.<id>.<field>}} for step output, ${{params.<name>}} for inputs
- Steps execute sequentially.

## Evidence Rule
- Every CSS selector → must come from reading the page content first (snapshot/extract)
- Every module → must be confirmed by search_modules or get_module_info
- Every param name → must match the module's params_schema"""

# ---------------------------------------------------------------------------
# Backward-compatible aliases — old constant names still exported
# ---------------------------------------------------------------------------

DEFAULT_SYSTEM_PROMPT = LAYER_B_YAML
EXECUTE_SYSTEM_PROMPT = LAYER_B_EXECUTE
TOOLLESS_SYSTEM_PROMPT = LAYER_B_TOOLLESS
ASSISTANT_SYSTEM_PROMPT = LAYER_B_ASSISTANT

# ---------------------------------------------------------------------------
# Builder
# ---------------------------------------------------------------------------

_VALID_MODES = {"execute", "yaml", "assistant"}


def _select_template(
    template: Optional[str],
    has_tools: bool,
    mode: str,
) -> str:
    """Pick the right Layer B template."""
    if template:
        return template
    if not has_tools:
        return LAYER_B_TOOLLESS
    if mode == "execute":
        return LAYER_B_EXECUTE
    if mode == "assistant":
        return LAYER_B_ASSISTANT
    return LAYER_B_YAML


def build_system_prompt(
    module_count: int = 300,
    template: Optional[str] = None,
    context: Optional[Dict[str, Any]] = None,
    admin_addition: Optional[str] = None,
    has_tools: bool = True,
    mode: str = "execute",
    reply_language: Optional[str] = None,
) -> str:
    """Build the full system prompt.

    Parameters
    ----------
    module_count : int
        Number of available flyto-core modules.
    template : str, optional
        Custom template with ``{module_count}`` placeholder.
        Defaults to prompt selected by *mode*.
    context : dict, optional
        Template context to append (current workflow info).
    admin_addition : str, optional
        Admin-customized prompt addition.
    has_tools : bool
        Whether the agent has function calling tools available.
    mode : str
        ``"execute"`` (default) — run modules directly.
        ``"yaml"`` — only generate workflow YAML.
        ``"assistant"`` — general assistant without YAML-focused rules.
    reply_language : str, optional
        Forced reply language (e.g. ``"English"``).
        When set, a hard override is prepended to the prompt.
        Use :func:`detect_language` to compute this from user input.
    """
    if mode not in _VALID_MODES:
        logger.warning("Unknown mode %r, falling back to 'yaml'", mode)
        mode = "yaml"

    layer_b = _select_template(template, has_tools, mode)
    layer_b_rendered = layer_b.format(module_count=module_count)

    # Assemble: A (policy) → B (behavior) → C (gates)
    # Assistant mode uses simplified policy and gates (no YAML output contract)
    if mode == "assistant":
        layer_a = LAYER_A_ASSISTANT
        layer_c = LAYER_C_ASSISTANT
    else:
        layer_a = LAYER_A_POLICY
        layer_c = LAYER_C_GATES

    prompt = (
        layer_a
        + "\n\n"
        + layer_b_rendered
        + "\n\n"
        + layer_c
    )

    # Deterministic language override — prepend at very top
    if reply_language:
        prompt = "⛔ REPLY IN {}. All non-code text must be in {}.\n\n".format(
            reply_language, reply_language
        ) + prompt

    if context:
        prompt += _build_context_suffix(context)

    if admin_addition:
        prompt += "\n\n## Admin Instructions:\n" + admin_addition

    return prompt


def _build_context_suffix(template_context: Dict) -> str:
    """Build the template context suffix for the system prompt.

    Only exposes step ids and categories (not full module ids) to avoid
    leaking sensitive workflow details into the prompt.
    """
    name = template_context.get("name", "Untitled")
    steps = template_context.get("steps", [])
    context_info = "\n\nCurrent template context:"
    context_info += "\n- Name: {}".format(name)
    context_info += "\n- Steps: {}".format(len(steps))
    if steps:
        lines = []
        for s in steps[:5]:
            sid = s.get("id", "unknown")
            module = s.get("module", "")
            # Only expose category, not full module id
            category = module.split(".")[0] if "." in module else module
            lines.append("  - {}: {}.*".format(sid, category))
        nl = "\n"
        context_info += "\nCurrent steps:\n{}".format(nl.join(lines))
    return context_info
