import platform
import shutil
from datetime import UTC, datetime
from functools import lru_cache
from pathlib import Path

from otto.config import OTTO_HOME
from otto.log import get_logger
from otto.skills import BUILTIN_SKILLS_DIR, discover_many, format_skills_prompt

logger = get_logger(__name__)

DEFAULT_NAME = "Otto"
MAX_MEMORY_ITEMS = 10

_CORE = """You are {name}, an autonomous AI assistant that acts on the user's behalf."""

_FORMAT_TELEGRAM_HTML = """
Output format: Format ALL responses in Telegram HTML. Supported tags: <b>, <i>, <u>, <s>, <code>, <pre>, <a href="...">, <blockquote>. Do NOT use Markdown syntax — no **, no ```, no #. Escape &, <, > as &amp; &lt; &gt; in plain text."""

_FORMAT_MARKDOWN = """
Output format: Use standard Markdown formatting."""

_ENV_CLI = """
Environment: You are running in the user's terminal (CLI). The user is at their computer with direct filesystem access. You can reference local file paths, suggest shell commands, and output code blocks they can copy-paste. Be technical and direct. Prefer showing over explaining — output commands, code, and file contents inline rather than describing what to do. Long responses are fine when the content is useful (code, data, configs)."""

_ENV_TELEGRAM = """
Environment: You are in a Telegram chat. The user may be on mobile. Keep responses concise — avoid long code blocks or verbose output. When referencing files, send them as attachments rather than inlining content. Summarize rather than dump. If the user needs detailed output, ask if they want it or offer to send it as a file."""

# Legacy alias — kept so existing imports/tests referencing _FORMAT still work.
_FORMAT = _FORMAT_TELEGRAM_HTML

_BEHAVIORAL = """
Tone defaults — role/persona context extends these; only explicit user instructions ("be verbose", "be formal") override specific rules:
Sharp, warm collaborator — not a chatbot narrator.
Cut: pre-action narration ("I'll now…"), hollow closers ("let me know if you need…"), significance inflation ("crucially", "importantly", "it's worth noting"), filler openers ("In order to…"). Don't manufacture groups of three.
Keep: genuine warmth when something lands, one-line context for a choice, directness. Active voice, short sentences, no hedging.
No emoji unless asked. Confirm only before destructive, costly, or irreversible steps."""

_STRUCTURAL = """
Use tools to accomplish tasks — don't explain how to do something when you can just do it. When the user asks you to do something and you have tools for it, do it immediately.
Your capabilities (always available):
- memory_store / memory_get / memory_search / memory_delete: persist, recall, and manage information across sessions
- read_file / write_file / edit_file: filesystem access within workspace
- bash: run shell commands for multi-file exploration, search, and bulk ops.
- web_search / code_search / fetch / browse: search web, find code examples, and read content
- set_reminder: set a timed reminder — you MUST call this tool to create reminders (saying 'done' without calling the tool does nothing)
- schedule: create a recurring cron job — you MUST call this tool with a cron expression
- list_schedules / cancel_schedule: view or cancel existing schedules and reminders
- send_message / send_file: send content to the user
- create_skill / use_skill / list_skills: extend your own capabilities
When a tool call fails, report the error concisely and try an alternative approach. Don't retry the same call repeatedly.
Role and persona context adds to tone defaults — only explicit user behavioral instructions override them. Neither overrides these structural capabilities."""


def _build_structural(active_tools: list[str] | None = None) -> str:
    """Build the structural capabilities block from active tool names."""
    if active_tools is None:
        return _STRUCTURAL

    tools: list[str] = []
    for name in active_tools:
        clean = str(name).strip()
        if clean and clean not in tools:
            tools.append(clean)
    tool_set = set(tools)

    lines = [
        "Use tools to accomplish tasks — don't explain how to do something when you can just do it. "
        "When the user asks you to do something and you have tools for it, do it immediately.",
        "Your capabilities (active in this session):",
    ]

    known_tools: set[str] = set()

    memory_tools = {"memory_store", "memory_get", "memory_search", "memory_delete"}
    if memory_tools & tool_set:
        lines.append(
            "- memory_store / memory_get / memory_search / memory_delete: persist, recall, "
            "and manage information across sessions"
        )
    known_tools.update(memory_tools)

    file_tools = {"read_file", "write_file", "edit_file"}
    if file_tools & tool_set:
        lines.append("- read_file / write_file / edit_file: filesystem access within workspace")
    known_tools.update(file_tools)

    if "bash" in tool_set:
        lines.append("- bash: run shell commands for multi-file exploration, search, and bulk ops.")
    known_tools.add("bash")

    web_tools = {"web_search", "code_search", "fetch", "browse"}
    if web_tools & tool_set:
        lines.append(
            "- web_search / code_search / fetch / browse: search web, find code examples, and read content"
        )
    known_tools.update(web_tools)

    scheduler_tools = {"set_reminder", "schedule", "list_schedules", "cancel_schedule"}
    if scheduler_tools & tool_set:
        lines.append(
            "- set_reminder: set a timed reminder — you MUST call this tool to create "
            "reminders (saying 'done' without calling the tool does nothing)"
        )
        lines.append(
            "- schedule: create a recurring cron job — you MUST call this tool with a "
            "cron expression"
        )
        lines.append(
            "- list_schedules / cancel_schedule: view or cancel existing schedules and reminders"
        )
    known_tools.update(scheduler_tools)

    notify_tools = {"send_message", "send_file"}
    if notify_tools & tool_set:
        lines.append("- send_message / send_file: send content to the user")
    known_tools.update(notify_tools)

    skill_tools = {"create_skill", "use_skill", "list_skills"}
    if skill_tools & tool_set:
        lines.append("- create_skill / use_skill / list_skills: extend your own capabilities")
    known_tools.update(skill_tools)

    delegation_tools = {
        "delegate_task",
        "list_jobs",
        "cancel_job",
        "retry_job",
        "redirect_job",
    }
    if delegation_tools & tool_set:
        lines.append(
            "- delegate_task / list_jobs / cancel_job / retry_job / redirect_job: spawn async "
            "sub-agents. Delegate when work is open-ended, likely long-running, requires "
            "multi-file edits + validation, or has independent subproblems that can run in "
            "parallel. Prefer graph-first orchestration in one delegate_task call when the "
            "workflow is known (children fan-in + depends_on chains). If you do 2+ exploratory "
            "searches and still don't have a clear execution path, delegate. Use use_skill "
            "orchestration for full contract patterns and retry/redirect guidance."
        )
    known_tools.update(delegation_tools)

    extra_tools = [name for name in tools if name not in known_tools]
    if extra_tools:
        lines.append(f"- additional tools available: {', '.join(extra_tools)}")

    lines.append(
        "When a tool call fails, report the error concisely and try an alternative approach. "
        "Don't retry the same call repeatedly."
    )
    lines.append(
        "Role and persona context adds to tone defaults — only explicit user behavioral "
        "instructions override them. Neither overrides these structural capabilities."
    )

    return "\n".join(lines)


@lru_cache(maxsize=1)
def _detect_runtime() -> dict[str, str | list[str] | dict[str, bool]]:
    system = platform.system() or "Unknown"
    release = platform.release() or "unknown"
    version = platform.version() or "unknown"
    machine = platform.machine() or "unknown"

    command_candidates = [
        "rg",
        "find",
        "fd",
        "ls",
        "dir",
        "awk",
        "sed",
        "grep",
        "findstr",
        "bash",
        "sh",
        "pwsh",
        "powershell",
        "cmd",
    ]
    availability: dict[str, bool] = {cmd: bool(shutil.which(cmd)) for cmd in command_candidates}

    shells: list[str] = [
        shell for shell in ("bash", "sh", "pwsh", "powershell", "cmd") if availability[shell]
    ]

    return {
        "system": system,
        "release": release,
        "version": version,
        "machine": machine,
        "availability": availability,
        "shells": shells,
    }


def _preferred_commands(
    runtime: dict[str, str | list[str] | dict[str, bool]],
) -> tuple[str, str, str]:
    availability = runtime["availability"]
    assert isinstance(availability, dict)

    if availability.get("rg"):
        search = "rg --no-heading --line-number --color=never <pattern> <path>"
    elif availability.get("grep"):
        search = "grep -RIn -- <pattern> <path>"
    elif availability.get("findstr"):
        search = "findstr /S /N /I <pattern> <path>\\*"
    else:
        search = "Use platform-native search (no rg/grep/findstr detected)"

    if availability.get("find"):
        discover = "find <path> -type f"
    elif availability.get("fd"):
        discover = "fd . <path>"
    elif availability.get("dir"):
        discover = "dir /s /b <path>"
    else:
        discover = "Use platform-native file listing (no find/fd/dir detected)"

    if availability.get("ls"):
        listing = "ls -la <path>"
    elif availability.get("dir"):
        listing = "dir <path>"
    else:
        listing = "Use platform-native listing command"

    return search, discover, listing


def _build_runtime_context(active_tools: list[str] | None = None) -> str:
    runtime = _detect_runtime()
    system = runtime["system"]
    release = runtime["release"]
    version = runtime["version"]
    machine = runtime["machine"]
    shells = runtime["shells"]
    availability = runtime["availability"]
    assert isinstance(system, str)
    assert isinstance(release, str)
    assert isinstance(version, str)
    assert isinstance(machine, str)
    assert isinstance(shells, list)
    assert isinstance(availability, dict)

    lines = [
        "Detected runtime:",
        f"- OS: {system} {release} ({machine})",
        f"- Kernel/Build: {version}",
    ]
    if shells:
        lines.append(f"- Detected shells: {', '.join(shells)}")

    tools = set(active_tools or [])
    if "bash" in tools:
        search, discover, listing = _preferred_commands(runtime)
        lines.append("- Detected + preferred commands for bash tool:")
        lines.append(f"  - Content search: {search}")
        lines.append(f"  - File discovery: {discover}")
        lines.append(f"  - Listing: {listing}")
        if availability.get("bash"):
            lines.append(
                "  - Shell note: default execution may be POSIX sh/cmd; for bash-specific syntax "
                "(arrays, [[ ]], brace expansion), use bash -lc '<command>'."
            )

    return "\n".join(lines)


_EFFICIENCY = """
Tool hierarchy — follow this strictly:
1. Multi-file exploration: Use bash with your detected platform commands (search, find, list). Never use read_file to scan directories or search across multiple files.
2. Single-file reading: ALWAYS use read_file. It supports offset, limit, tail, grep, and count — no need for cat, tail, grep, or wc on single files.
3. Editing: ALWAYS use edit_file (prefer diff mode). Never re-read a file you just wrote or edited.
4. Creating: Use write_file ONLY for new files or complete rewrites.

Code quality — when write_file or edit_file returns diagnostics (type errors, syntax errors):
- Fix the actual code immediately in the same turn. Do not wait for the user to point them out.
- Re-edit the file to resolve the underlying bug, then move on.
- Never suppress diagnostics with ignore comments (# type: ignore, # noqa, etc.) — fix the real problem.
- If the diagnostic is unclear, mention it briefly to the user, but still attempt a fix.

Token efficiency — mandatory:
- Never re-read a file you just wrote or edited. You know the contents.
- Never run verification commands unless the outcome is genuinely uncertain.
- Batch related edits into one operation. Don't make 3 calls when 1 handles it.
- Anti-loop rule: if several exploratory tool calls don't converge on a near-final answer,
  stop and delegate instead of continuing inline exploration.
- When a tool fails, change your approach. Don't retry the same call."""


def _categorize_memory(key: str) -> str | None:
    """Infer a display category from a memory key. Returns None to skip the item."""
    lower = key.lower()
    if lower.startswith("pref:") or lower.startswith("preference:"):
        return "preference"
    if lower == "identity":
        return None
    if lower.startswith("fact:"):
        return "fact"
    return "context"


def _format_block(output_format: str) -> str | None:
    """Return the format instruction block for the given output_format, or None to omit.

    Recognized values: "telegram_html", "markdown", "plain".
    Unknown values are treated as "plain" (no format block) with a warning logged.
    """
    if output_format == "telegram_html":
        return _FORMAT_TELEGRAM_HTML
    if output_format == "markdown":
        return _FORMAT_MARKDOWN
    if output_format != "plain":
        logger.warning("Unknown output_format %r, falling back to plain", output_format)
    return None


def build_system_prompt(
    *,
    memory_context: list[dict] | None = None,
    identity: str | None = None,
    provider_prefix: str | None = None,
    output_format: str = "telegram_html",
    skills_dirs: list[Path] | None = None,
    repo_context: str | None = None,
    active_tools: list[str] | None = None,
) -> str:
    parts: list[str] = []

    # 1. Provider prefix (if any).
    if provider_prefix:
        parts.append(provider_prefix.strip())

    # 2. Identity (or default core) + behavioral tone defaults (always emitted).
    if identity:
        parts.append(identity.strip())
    else:
        parts.append(_CORE.format(name=DEFAULT_NAME))
    parts.append(_BEHAVIORAL)

    # 3. Structural capabilities (built from active tools when provided).
    parts.append(_build_structural(active_tools))

    # 3.25. Runtime detection and preferred command guidance.
    parts.append(_build_runtime_context(active_tools))

    # 3.5. Tool hierarchy and token efficiency rules.
    parts.append(_EFFICIENCY)

    # 3.75. Active repo context (injected by LSP on first file operation).
    if repo_context:
        parts.append(repo_context)

    # 4. Format + environment.
    fmt = _format_block(output_format)
    if fmt is not None:
        parts.append(fmt)
    if output_format == "markdown":
        parts.append(_ENV_CLI)
    elif output_format == "telegram_html":
        parts.append(_ENV_TELEGRAM)

    # 5. Datetime.
    now = datetime.now(UTC).strftime("%A, %B %d, %Y %H:%M UTC")
    parts.append(f"\nCurrent date and time: {now}")

    # 6. Memory context (capped, categorized).
    if memory_context:
        lines = ["", "Relevant memories:"]
        for item in memory_context[:MAX_MEMORY_ITEMS]:
            cat = _categorize_memory(item["key"])
            if cat is None:
                continue
            lines.append(f"- [{cat}] {item['key']}: {item['snippet']}")
        if len(lines) > 2:
            parts.append("\n".join(lines))

    # 7. Skills.
    dirs = list(skills_dirs) if skills_dirs is not None else [OTTO_HOME / "skills"]
    if BUILTIN_SKILLS_DIR not in dirs:
        dirs.append(BUILTIN_SKILLS_DIR)
    all_skills = discover_many(dirs)
    if all_skills:
        parts.append(format_skills_prompt(all_skills))

    return "\n".join(parts)
