"""
BIBIVIDI — Script Generator
When no BSS script matches a prompt, calls an LLM to generate a new BSS skill.
The generated skill is embedded and saved to the library for future requests.
"""
from __future__ import annotations

import json
import logging
import re
import uuid

import httpx

from bvd.config import settings
from bvd.engine.library import Script, library
from bvd.engine.matcher import matcher

logger = logging.getLogger(__name__)

SYSTEM_PROMPT = """\
You are a BIBIVIDI skill generator. Analyze a user prompt and create a reusable
Python preprocessing script (BSS skill) that reduces token count for similar prompts.

## What the generated script must do
- Read the user's prompt from the variable `PROMPT` (already injected — do NOT read stdin)
- Extract only the essential information the LLM needs to answer
- Print the condensed result to stdout
- Target 60-95% token reduction while preserving semantic meaning

## Strict sandbox constraints
Allowed imports ONLY: json, re, math, statistics, datetime, collections,
itertools, functools, pathlib, string, textwrap, csv, io, base64, hashlib, uuid

FORBIDDEN: os, sys, subprocess, socket, urllib, requests, httpx, or any external library.
Do NOT call open(), do NOT access the filesystem.

## Output format — return ONLY valid JSON, no markdown, no explanation:
{
  "skill_json": {
    "id": "kebab-case-id",
    "name": "Human Readable Name",
    "description": "One sentence: what this skill preprocesses and the expected token saving",
    "tags": ["tag1", "tag2"],
    "version": "1.0.0",
    "author": "bibividi-generator"
  },
  "skill_py": "# Python code here\\n..."
}"""


class Generator:
    def __init__(self) -> None:
        self._client: httpx.AsyncClient | None = None

    async def _get_client(self) -> httpx.AsyncClient:
        if self._client is None or self._client.is_closed:
            self._client = httpx.AsyncClient(timeout=httpx.Timeout(60.0))
        return self._client

    async def generate(self, prompt: str) -> Script | None:
        """Call LLM to generate a new BSS skill for the given prompt type.

        Returns the saved Script on success, None on any failure.
        """
        if not settings.generator_enabled:
            return None
        if not settings.generator_api_key:
            logger.debug("Generator skipped: BIBIVIDI_GENERATOR_API_KEY not set")
            return None

        logger.info("Generating BSS skill for prompt (%d chars)…", len(prompt))

        try:
            raw = await self._call_llm(prompt)
        except Exception as exc:
            logger.warning("Generator LLM call failed: %s", exc)
            return None

        skill_json, skill_py = _parse_response(raw)
        if not skill_json or not skill_py:
            logger.warning("Generator returned unparseable response")
            return None

        try:
            script = await library.import_bss(skill_json, skill_py, status="pending")
            script = await matcher.embed_script(script)
            await library.save(script)
            matcher.invalidate()
            logger.info(
                "New skill saved (pending approval): %s — %s  "
                "Run 'bibividi review' to inspect and approve.",
                script.id, script.name,
            )
            return script
        except Exception as exc:
            logger.warning("Failed to save generated skill: %s", exc)
            return None

    async def maybe_generate(self, prompt: str) -> None:
        """Non-blocking wrapper: generate a skill without blocking the current request."""
        try:
            await self.generate(prompt)
        except Exception as exc:
            logger.debug("maybe_generate unexpected error: %s", exc)

    # ── LLM call ──────────────────────────────────────────────────────────────

    async def _call_llm(self, prompt: str) -> str:
        assert settings.generator_api_key is not None
        client = await self._get_client()
        # SECURITY: the first 2000 chars of the user's prompt are sent to the LLM.
        # A crafted prompt could attempt instruction injection. This is mitigated by:
        # (1) the sandbox blocks all network/filesystem access in the generated skill, and
        # (2) the approval gate requires human review before any generated skill executes.
        user_content = (
            "Analyze this prompt and generate a BSS skill to preprocess similar prompts:\n\n"
            f"---\n{prompt[:2000]}\n---\n\n"
            "Return only the JSON object."
        )
        if settings.generator_provider == "openai":
            return await self._call_openai(client, user_content)
        return await self._call_anthropic(client, user_content)

    async def _call_anthropic(self, client: httpx.AsyncClient, user_content: str) -> str:
        assert settings.generator_api_key is not None
        resp = await client.post(
            f"{settings.generator_api_base}/v1/messages",
            headers={
                "x-api-key": settings.generator_api_key,
                "anthropic-version": "2023-06-01",
                "content-type": "application/json",
            },
            json={
                "model": settings.generator_model,
                "max_tokens": 2048,
                "system": SYSTEM_PROMPT,
                "messages": [{"role": "user", "content": user_content}],
            },
        )
        resp.raise_for_status()
        return resp.json()["content"][0]["text"]

    async def _call_openai(self, client: httpx.AsyncClient, user_content: str) -> str:
        resp = await client.post(
            f"{settings.generator_api_base}/v1/chat/completions",
            headers={
                "Authorization": f"Bearer {settings.generator_api_key}",
                "content-type": "application/json",
            },
            json={
                "model": settings.generator_model,
                "max_tokens": 2048,
                "messages": [
                    {"role": "system", "content": SYSTEM_PROMPT},
                    {"role": "user", "content": user_content},
                ],
            },
        )
        resp.raise_for_status()
        return resp.json()["choices"][0]["message"]["content"]


# ── Helpers ───────────────────────────────────────────────────────────────────

def _parse_response(raw: str) -> tuple[dict | None, str | None]:
    """Extract skill_json and skill_py from the LLM response."""
    # Strip markdown fences if the model wrapped the JSON
    raw = re.sub(r"```(?:json)?\s*", "", raw)
    raw = re.sub(r"```\s*$", "", raw, flags=re.MULTILINE)
    raw = raw.strip()

    try:
        data = json.loads(raw)
        skill_json = data.get("skill_json")
        skill_py = data.get("skill_py")

        if not isinstance(skill_json, dict) or not isinstance(skill_py, str):
            return None, None

        # Guarantee a safe slug id
        raw_id = skill_json.get("id", "")
        if not raw_id or not re.match(r"^[a-z0-9][a-z0-9\-]{1,48}[a-z0-9]$", raw_id):
            skill_json["id"] = f"generated-{uuid.uuid4().hex[:8]}"

        return skill_json, skill_py

    except (json.JSONDecodeError, KeyError, TypeError):
        return None, None


# Singleton
generator = Generator()
