# SPDX-License-Identifier: GPL-2.0-only
#
# llamachat - a small native chat client for a local llama.cpp router
# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
"""System prompts kept as plain files.

One markdown file per prompt in the prompts directory. `default.md` is the
global prompt; every other file is a named preset that *replaces* it rather
than adding to it. A conversation may also carry its own text, or opt out of
a system prompt entirely.

Files rather than a table because these are things you want to edit in an
editor, diff, and keep in version control.
"""

import re
from dataclasses import dataclass
from pathlib import Path

from .config import GLOBAL_PROMPT

# Sentinels stored on a session in place of a prompt name: NONE sends no
# system prompt at all, CUSTOM means the session carries its own text.
NONE = "none"
CUSTOM = "custom"

DEFAULT_TEXT = (
    "You are a helpful assistant running locally. Be concise and direct.\n"
)

# Conservative: these names become filenames.
_SAFE_NAME = re.compile(r"[^A-Za-z0-9._-]")


@dataclass
class Prompt:
    name: str
    text: str
    path: Path

    @property
    def is_global(self) -> bool:
        return self.name == GLOBAL_PROMPT


def safe_name(name: str) -> str:
    """Reduce a display name to something usable as a filename.

    Path separators and dots are stripped, so a name like "../../etc/passwd"
    cannot escape the prompts directory. A name that reduces to nothing
    yields "" and callers must treat that as invalid.
    """
    cleaned = _SAFE_NAME.sub("-", name.strip()).strip("-.")
    return cleaned[:64]


class PromptStore:
    """The prompts directory, read and written as individual files."""

    def __init__(self, directory: Path):
        self.dir = directory

    def ensure_default(self) -> None:
        """Create the directory and a starter global prompt if absent."""
        self.dir.mkdir(parents=True, exist_ok=True)
        target = self.path_for(GLOBAL_PROMPT)
        if not target.exists():
            target.write_text(DEFAULT_TEXT, encoding="utf-8")

    def path_for(self, name: str) -> Path:
        cleaned = safe_name(name)
        if not cleaned:
            raise ValueError(f"unusable prompt name: {name!r}")
        return self.dir / f"{cleaned}.md"

    def names(self) -> list[str]:
        """Prompt names, global first, then presets alphabetically."""
        if not self.dir.is_dir():
            return []
        found = sorted(
            p.stem for p in self.dir.glob("*.md") if p.is_file()
        )
        if GLOBAL_PROMPT in found:
            found.remove(GLOBAL_PROMPT)
            return [GLOBAL_PROMPT] + found
        return found

    def load(self, name: str) -> Prompt | None:
        try:
            path = self.path_for(name)
            text = path.read_text(encoding="utf-8")
        except (OSError, ValueError):
            return None
        return Prompt(name=safe_name(name), text=text, path=path)

    def save(self, name: str, text: str) -> Prompt:
        self.dir.mkdir(parents=True, exist_ok=True)
        path = self.path_for(name)
        path.write_text(text, encoding="utf-8")
        return Prompt(name=safe_name(name), text=text, path=path)

    def delete(self, name: str) -> bool:
        """Remove a preset. The global prompt is never deleted."""
        if safe_name(name) == GLOBAL_PROMPT:
            return False
        try:
            path = self.path_for(name)
        except ValueError:
            return False
        if not path.exists():
            return False
        path.unlink()
        return True

    def resolve(self, choice: str, custom: str = "") -> str:
        """The system prompt text a conversation should actually send.

        `choice` is a prompt name, the NONE sentinel, or empty for the
        global default. A named preset replaces the global prompt rather
        than being appended to it.
        """
        if choice == NONE:
            return ""
        if choice == "":
            choice = GLOBAL_PROMPT
        if choice == CUSTOM:
            return custom.strip()
        prompt = self.load(choice)
        if prompt is None:
            # A preset that was deleted behind our back falls back to the
            # global prompt rather than silently sending nothing.
            prompt = self.load(GLOBAL_PROMPT)
        return prompt.text.strip() if prompt else ""
