"""
Prompt — stores, versions, and renders prompt templates.
No LLM calls. No API keys. Bring your own model.
"""

import hashlib
from datetime import datetime
from pathlib import Path
from string import Formatter
from typing import Any, Dict, List, Optional

import yaml

from .storage import PromptStorage


class Prompt:
    """
    A named, versioned prompt template.

    PromptVault never calls any LLM. You call your own LLM,
    then pass the response back for logging and comparison.

    Example:
        >>> p = Prompt("summarize", template="Summarize this: {text}")
        >>> rendered = p.render(text="The sky is blue.")
        >>> response = your_llm(rendered)           # you do this
        >>> p.log(rendered, response)               # we track this
    """

    def __init__(
        self,
        name: str,
        template: str,
        version: str = "v1",
        description: str = "",
        tags: Optional[List[str]] = None,
        metadata: Optional[Dict[str, Any]] = None,
        vault_dir: Optional[str] = None,
    ):
        self.name = name
        self.template = template
        self.version = version
        self.description = description
        self.tags = tags or []
        self.metadata = metadata or {}
        self.created_at = datetime.utcnow().isoformat()
        self.hash = self._compute_hash()

        self._vault_dir = Path(vault_dir) if vault_dir else Path(".promptvault")
        self._storage = PromptStorage(self._vault_dir)
        self._storage.save(self._to_dict())

    def _compute_hash(self) -> str:
        return hashlib.md5(self.template.encode()).hexdigest()[:8]

    def _to_dict(self) -> Dict[str, Any]:
        return {
            "name": self.name,
            "version": self.version,
            "description": self.description,
            "template": self.template,
            "tags": self.tags,
            "metadata": self.metadata,
            "created_at": self.created_at,
            "hash": self.hash,
        }

    # ------------------------------------------------------------------
    # Rendering
    # ------------------------------------------------------------------

    def render(self, **kwargs) -> str:
        """
        Fill in the template variables and return the final prompt string.

        Example:
            p = Prompt("greet", "Hello {name}, you are {age} years old.")
            text = p.render(name="Alice", age=30)
        """
        missing = [v for v in self.variables() if v not in kwargs]
        if missing:
            raise ValueError(
                f"Missing variables for prompt '{self.name}': {missing}. "
                f"Required: {self.variables()}"
            )
        return self.template.format(**kwargs)

    def variables(self) -> List[str]:
        """Return the list of variables in this template."""
        return [
            fname
            for _, fname, _, _ in Formatter().parse(self.template)
            if fname is not None
        ]

    # ------------------------------------------------------------------
    # Logging
    # ------------------------------------------------------------------

    def log(
        self,
        rendered_prompt: str,
        response: str,
        model: Optional[str] = None,
        latency_ms: Optional[float] = None,
        tokens: Optional[int] = None,
        extra: Optional[Dict[str, Any]] = None,
    ) -> str:
        """
        Log a prompt/response pair to the vault.

        You call your LLM. You pass the response here.
        PromptVault stores it for future comparison and analysis.

        Args:
            rendered_prompt: The final prompt string you sent to the LLM.
            response:        The response the LLM returned.
            model:           Optional — name of the model you used.
            latency_ms:      Optional — how long the call took in milliseconds.
            tokens:          Optional — token count if your LLM provides it.
            extra:           Optional — any other metadata you want to store.

        Returns:
            run_id: A unique ID for this logged run.
        """
        return self._storage.log_run(
            prompt_name=self.name,
            prompt_version=self.version,
            rendered_prompt=rendered_prompt,
            response=response,
            model=model,
            latency_ms=latency_ms,
            tokens=tokens,
            extra=extra or {},
        )

    # ------------------------------------------------------------------
    # Stats & History
    # ------------------------------------------------------------------

    def stats(self) -> Dict[str, Any]:
        """Return aggregated stats for all logged runs of this prompt."""
        return self._storage.get_stats(self.name, self.version)

    def runs(self, last_n: int = 50) -> List[Dict[str, Any]]:
        """Return recent logged runs for this prompt."""
        return self._storage.get_runs(self.name, self.version, last_n)

    def history(self) -> List[Dict[str, Any]]:
        """Return all saved versions of this prompt."""
        return self._storage.get_versions(self.name)

    # ------------------------------------------------------------------
    # Versioning
    # ------------------------------------------------------------------

    def update(self, new_template: str, version: Optional[str] = None) -> "Prompt":
        """
        Create a new version of this prompt with an updated template.

        The old version is preserved in history automatically.

        Args:
            new_template: The new prompt template string.
            version:      Optional version label. Auto-increments if not given.

        Returns:
            A new Prompt instance with the bumped version.
        """
        if version is None:
            try:
                num = int(self.version.lstrip("v"))
                version = f"v{num + 1}"
            except ValueError:
                version = self.version + "-updated"

        return Prompt(
            name=self.name,
            template=new_template,
            version=version,
            description=self.description,
            tags=self.tags,
            metadata=self.metadata,
            vault_dir=str(self._vault_dir),
        )

    # ------------------------------------------------------------------
    # YAML Import / Export
    # ------------------------------------------------------------------

    def save(self, path: Optional[str] = None) -> str:
        """
        Save this prompt to a YAML file.

        Args:
            path: File path. Defaults to prompts/{name}.yaml

        Returns:
            Path to the saved file.
        """
        out = Path(path) if path else Path(f"prompts/{self.name}.yaml")
        out.parent.mkdir(parents=True, exist_ok=True)
        with open(out, "w") as f:
            yaml.dump(self._to_dict(), f, default_flow_style=False, allow_unicode=True)
        return str(out)

    @classmethod
    def load(cls, path: str, vault_dir: Optional[str] = None) -> "Prompt":
        """
        Load a prompt from a YAML file.

        Args:
            path:      Path to the YAML file.
            vault_dir: Optional vault directory override.

        Returns:
            A Prompt instance.
        """
        with open(path) as f:
            data = yaml.safe_load(f)
        return cls(
            name=data["name"],
            template=data["template"],
            version=data.get("version", "v1"),
            description=data.get("description", ""),
            tags=data.get("tags", []),
            metadata=data.get("metadata", {}),
            vault_dir=vault_dir,
        )

    # ------------------------------------------------------------------
    # Dunder
    # ------------------------------------------------------------------

    def __repr__(self) -> str:
        return (
            f"Prompt(name='{self.name}', version='{self.version}', "
            f"variables={self.variables()}, hash='{self.hash}')"
        )

    def __str__(self) -> str:
        return self.template
