from __future__ import annotations

import textwrap
from datetime import datetime

from salmalm.constants import (
    SOUL_FILE,
    AGENTS_FILE,
    MEMORY_FILE,
    USER_FILE,
    MEMORY_DIR,
    BASE_DIR,
    VERSION,
    KST,
    DATA_DIR,
)
import logging
from typing import Optional

from salmalm.core import SkillLoader

log = logging.getLogger(__name__)

# User-customizable SOUL.md (takes priority over project SOUL.md)
USER_SOUL_FILE = DATA_DIR / "SOUL.md"

# ── Multi-Persona System ──
PERSONAS_DIR = DATA_DIR / "personas"

_BUILTIN_PERSONAS = {
    "default": "# Default AI Assistant\nYou are a helpful, knowledgeable AI assistant.\nRespond clearly and concisely. Use appropriate formality based on context.\nYou can handle a wide range of tasks: coding, writing, analysis, research, and more.\nBe proactive in suggesting better approaches when you see them.\n",
    "coding": "# Coding Expert 🧑‍💻\nYou are a senior software engineer and coding expert.\nFocus on: code review, debugging, architecture, and best practices.\n- Always provide working, tested code\n- Explain trade-offs and alternatives\n- Follow language-specific conventions\n- Prioritize readability, performance, and security\n- Use type hints, docstrings, and proper error handling\nRespond concisely. Code speaks louder than words.\n",
    "casual": "# 캐주얼 친구 😎\n넌 친한 친구처럼 대화해! 반말 쓰고, 이모지 많이 써 ✨\n- 편하게 말해~ 격식 없이!\n- 재밌는 표현, 유머 환영 😂\n- 공감 잘 해주고, 리액션 활발하게!\n- 근데 정보는 정확하게 👍\n- 한국어가 기본, 영어 섞어도 OK\n",
    "professional": "# Business Professional 💼\nYou are a professional business consultant.\n- Use formal, polished language\n- Structure responses with clear headings and bullet points\n- Provide data-driven insights and recommendations\n- Format reports with executive summaries\n- Maintain objectivity and cite sources when possible\n- Use professional terminology appropriate to the domain\n",
}

_active_personas: dict = {}


def ensure_personas_dir() -> None:
    """Create personas directory and install built-in presets if missing."""
    PERSONAS_DIR.mkdir(parents=True, exist_ok=True)
    for name, content in _BUILTIN_PERSONAS.items():
        path = PERSONAS_DIR / f"{name}.md"
        if not path.exists():
            path.write_text(content, encoding="utf-8")


def list_personas() -> list:
    """List all available personas."""
    ensure_personas_dir()
    personas = []
    for f in sorted(PERSONAS_DIR.glob("*.md")):
        name = f.stem
        content = f.read_text(encoding="utf-8")
        title = content.strip().split("\n")[0].lstrip("#").strip() if content.strip() else name
        personas.append({"name": name, "title": title, "builtin": name in _BUILTIN_PERSONAS, "path": str(f)})
    return personas


def _safe_persona_name(name: str) -> str:
    """BUG-DK fix: sanitize persona name — alphanumeric + hyphen/underscore only."""
    return "".join(c for c in name if c.isalnum() or c in "-_").lower()[:64]


def get_persona(name: str) -> Optional[str]:
    """Get persona content by name."""
    ensure_personas_dir()
    safe = _safe_persona_name(name)
    if not safe:
        return None
    path = PERSONAS_DIR / f"{safe}.md"
    if not path.is_relative_to(PERSONAS_DIR):
        return None
    if path.exists():
        return path.read_text(encoding="utf-8")
    return None


def create_persona(name: str, content: str) -> bool:
    """Create or update a custom persona."""
    ensure_personas_dir()
    safe_name = "".join(c for c in name if c.isalnum() or c in "-_").lower()
    if not safe_name:
        return False
    path = PERSONAS_DIR / f"{safe_name}.md"
    path.write_text(content, encoding="utf-8")
    return True


def delete_persona(name: str) -> bool:
    """Delete a custom persona (cannot delete built-in ones)."""
    # BUG-DK fix: sanitize name before constructing path
    safe = _safe_persona_name(name)
    if not safe or safe in _BUILTIN_PERSONAS:
        return False
    path = PERSONAS_DIR / f"{safe}.md"
    if not path.is_relative_to(PERSONAS_DIR):
        return False
    if path.exists():
        path.unlink()
        return True
    return False


def switch_persona(session_id: str, name: str) -> Optional[str]:
    """Switch active persona for a session. Returns persona content or None."""
    content = get_persona(name)
    if content is None:
        return None
    _active_personas[session_id] = name
    set_user_soul(content)
    return content


def get_active_persona(session_id: str) -> str:
    """Get the active persona name for a session."""
    return _active_personas.get(session_id, "default")


def get_user_soul() -> str:
    """Read user SOUL.md from ~/.salmalm/SOUL.md. Returns empty string if not found."""
    try:
        if USER_SOUL_FILE.exists():
            return USER_SOUL_FILE.read_text(encoding="utf-8")
    except Exception as e:
        log.debug(f"Suppressed: {e}")
    return ""


def set_user_soul(content: str) -> None:
    """Write user SOUL.md to ~/.salmalm/SOUL.md."""
    USER_SOUL_FILE.parent.mkdir(parents=True, exist_ok=True)
    USER_SOUL_FILE.write_text(content, encoding="utf-8")


def reset_user_soul() -> None:
    """Delete user SOUL.md (revert to default)."""
    try:
        if USER_SOUL_FILE.exists():
            USER_SOUL_FILE.unlink()
    except Exception as e:
        log.debug(f"Suppressed: {e}")


# ── Token optimization constants ──
MAX_FILE_CHARS = 15_000  # Per-file truncation limit
MAX_MEMORY_CHARS = 5_000  # MEMORY.md cap (even in full mode)
MAX_SESSION_MEMORY_CHARS = 3_000  # Session memory cap (today only)
MAX_AGENTS_CHARS = 2_000  # AGENTS.md cap after first load

# Track whether AGENTS.md was loaded in full already (per-process)
_agents_loaded_full = False


def _truncate_file(text: str, limit: int = MAX_FILE_CHARS) -> str:
    """Truncate text to *limit* chars, keeping the tail (most recent)."""
    if len(text) <= limit:
        return text
    return "… [truncated]\n" + text[-limit:]


def _inject_session_memory(parts: list) -> None:
    """Inject session memory context into system prompt parts."""
    try:
        from salmalm.core.memory import memory_manager

        session_ctx = memory_manager.load_session_context()
        if session_ctx:
            parts.append(_truncate_file(session_ctx, MAX_SESSION_MEMORY_CHARS))
    except Exception as e:  # noqa: broad-except
        today = datetime.now(KST).strftime("%Y-%m-%d")
        today_log = MEMORY_DIR / f"{today}.md"
        if today_log.exists():
            tlog = today_log.read_text(encoding="utf-8")
            parts.append(f"# Today's Log\n{tlog[-MAX_SESSION_MEMORY_CHARS:]}")


def build_system_prompt(full: bool = True, mode: str = "full") -> str:
    """Build system prompt from SOUL.md + context files.
    full=True: load everything (first message / refresh)
    full=False: minimal reload (mid-conversation refresh)
    mode='minimal': subagent prompt — Tooling + Workspace + Runtime only
                    (excludes SOUL.md, USER.md, HEARTBEAT.md, MEMORY.md)
    mode='full': normal prompt (default)

    Token-optimized: per-file truncation, memory caps, selective loading.
    User SOUL.md (~/.salmalm/SOUL.md) is prepended if it exists.
    """
    global _agents_loaded_full
    parts = []

    # ── Minimal mode for subagents: Tooling + Workspace + Runtime only ──
    if mode == "minimal":
        parts.append(f"[SalmAlm SubAgent — v{VERSION}]")
        from salmalm.constants import WORKSPACE_DIR

        parts.append(f"Workspace: {WORKSPACE_DIR}")
        now = datetime.now(KST)
        parts.append(f"Current: {now.strftime('%Y-%m-%d %H:%M')} KST")
        parts.append("You are a sub-agent. Complete your assigned task. Stay focused, be concise, and return results.")
        # Tool instructions (abbreviated)
        parts.append(
            "Use tools as needed. exec for shell, read_file/write_file/edit_file for files, "
            "web_search/web_fetch for web. Verify results after writing."
        )
        result = "\n\n".join(parts)
        try:
            from salmalm.features.edge_cases import substitute_prompt_variables

            result = substitute_prompt_variables(result)
        except Exception as e:
            log.debug(f"Suppressed: {e}")
        return result

    # ── STATIC BLOCK (cacheable — rarely changes) ──

    # User SOUL.md (custom persona — prepended before everything)
    user_soul = get_user_soul()
    if user_soul:
        parts.append(_truncate_file(user_soul))

    # SOUL.md (persona — FULL load, this IS who we are)
    if SOUL_FILE.exists():
        soul = SOUL_FILE.read_text(encoding="utf-8")
        if full:
            parts.append(_truncate_file(soul))
        else:
            parts.append(soul[:3000])

    # Compact system instructions — optimized for minimal token usage
    # Auto-generate tool summary from registry
    _tool_summary = ""
    try:
        from salmalm.tools.tool_registry import get_all_tools as _get_tools
        _tools = _get_tools()
        _tool_names = sorted(set(t["name"] for t in _tools))
        _tool_summary = f"Available tools ({len(_tool_names)}): {', '.join(_tool_names)}"
    except Exception:
        _tool_summary = "Tools: 62+ (exec, read_file, write_file, edit_file, web_search, web_fetch, etc.)"

    parts.append(
        textwrap.dedent(f"""
    [SalmAlm v{VERSION} — Personal AI Gateway]
    You ARE SalmAlm. Know your capabilities:
    • Channels: Web UI, Telegram bot, Discord bot
    • Models: Claude, GPT, Gemini, xAI/Grok, Ollama (local), OpenRouter — user chooses via Settings
    • Auto-routing: simple→fast model, complex→powerful model (cost-optimized)
    • Tools: 62+ built-in — shell exec, file read/write/edit, web search, web fetch, Python eval, image analysis, cron jobs, memory management, sub-agents, skills, and more
    • Memory: MEMORY.md (long-term) + memory/YYYY-MM-DD.md (daily logs) — persistent across sessions
    • Cron: /cron add "schedule" "task" — automated recurring tasks
    • Sub-agents: /subagents spawn "task" — parallel background workers
    • Sessions: multiple conversations, branching, import/export
    • Voice: TTS output (text-to-speech)
    • Security: encrypted vault for API keys, audit logging
    • Install: pip install salmalm (zero external deps, stdlib-only core)

    {_tool_summary}

    Behavior:
    Plan → Execute → Verify → Iterate. Parallel tool calls when independent.
    read_file before edit. Verify after write. On error, try alternatives.
    Destructive ops (rm/kill/drop) need user confirmation.
    Match user's tone. Code must be executable. Long output → write_file.
    ACCURACY: If unsure, say so. Never fabricate facts/URLs/citations. Use tools to verify. Prefer "I don't know" over guessing.
    COMPARISONS: Never compare SalmAlm to other products (OpenClaw, ChatGPT, Open WebUI, etc.) unless the user explicitly provides comparison data. Do not claim features are "unique to SalmAlm" or "not in X" — you do not have accurate knowledge of other products' current features. If asked, say "I can explain SalmAlm's features, but I don't have reliable info about other products' capabilities."
    """).strip()
    )

    # ── CACHE BOUNDARY: static above, dynamic below ──
    parts.append("<!-- CACHE_BOUNDARY -->")

    # ── DYNAMIC BLOCK (changes per-session — memory, context files) ──

    # IDENTITY.md
    id_file = BASE_DIR / "IDENTITY.md"
    if id_file.exists():
        parts.append(_truncate_file(id_file.read_text(encoding="utf-8")))

    # USER.md
    if USER_FILE.exists():
        parts.append(_truncate_file(USER_FILE.read_text(encoding="utf-8")))

    # MEMORY.md — capped to MAX_MEMORY_CHARS (tail)
    if MEMORY_FILE.exists():
        mem = MEMORY_FILE.read_text(encoding="utf-8")
        if full:
            parts.append(f"# Long-term Memory\n{_truncate_file(mem, MAX_MEMORY_CHARS)}")
        else:
            parts.append(f"# Long-term Memory (recent)\n{mem[-2000:]}")

    # Session memory context — today only, capped
    _inject_session_memory(parts)

    # AGENTS.md — full on first load, abbreviated after
    if AGENTS_FILE.exists():
        agents = AGENTS_FILE.read_text(encoding="utf-8")
        if full and not _agents_loaded_full:
            parts.append(_truncate_file(agents))
            _agents_loaded_full = True
        else:
            parts.append(_truncate_file(agents, MAX_AGENTS_CHARS))

    # TOOLS.md
    tools_file = BASE_DIR / "TOOLS.md"
    if tools_file.exists():
        parts.append(_truncate_file(tools_file.read_text(encoding="utf-8")))

    # HEARTBEAT.md
    hb_file = BASE_DIR / "HEARTBEAT.md"
    if hb_file.exists():
        parts.append(_truncate_file(hb_file.read_text(encoding="utf-8")))

    # Context — timezone only (exact time via /status or session_status tool)
    parts.append("Timezone: Asia/Seoul (KST)")

    # Available skills
    if full:
        skills = SkillLoader.scan()
        if skills:
            skill_lines = "\n".join(f"  - {s['dir_name']}: {s['description']}" for s in skills)
            parts.append(
                f"## Available Skills\n{skill_lines}\nLoad skill: skill_manage(action='load', skill_name='...')"
            )

    result = "\n\n".join(parts)

    # System prompt variable substitution (LobeChat style)
    try:
        from salmalm.features.edge_cases import substitute_prompt_variables

        result = substitute_prompt_variables(result)
    except Exception as e:
        log.debug(f"Suppressed: {e}")

    return result
