"""AI Module Builder — Low-Code generator with Super-Admin guardrails.

The Super Admin types a natural-language request like:
    "Create a Vehicle Service Tracker with vehicle_no, service_type,
     date, mechanic, cost, status (Pending/Done)."
and an LLM (Claude Sonnet 4.5) returns a strict JSON spec. The spec is
stored in the `custom_modules` collection in `draft` state. Once approved
by the Super Admin it flips to `published` and a generic CRUD UI renders it
at `/custom/<slug>` — NO files are written to disk, NO real code is
generated. This keeps the system safe and reversible (just delete the spec).

GUARDRAILS the LLM is FORBIDDEN from producing:
  • Module that touches `users`, `companies`, `audit_log`, `permissions`,
    `backups`, `roles`, `licenses`, `items` (uses its own `custom_records.{slug}` collection).
  • Field types other than the allow-listed set (text, number, date, …).
  • Permissions that bypass the existing RBAC.

SUPER ADMIN CONTROL FLAGS (stored at `system_settings.ai_builder`):
  • enabled                 — kill switch (default: false)
  • allow_database_changes  — if false, AI can only generate UI specs (no new collections used)
  • require_approval        — if true, AI-generated spec stays `draft` until human approval
  • auto_deploy             — currently always false (governance)
"""
import json
import os
import re
from datetime import datetime, timezone
from typing import Optional, List, Dict, Any
from bson import ObjectId
from fastapi import APIRouter, HTTPException, Depends, Request, Query
from pydantic import BaseModel, Field

from auth import get_current_user, require_admin


router = APIRouter(prefix="/api/ai-builder", tags=["ai-builder"])


# =============== Helpers ===============
def _oid(v: str) -> ObjectId:
    try:
        return ObjectId(v)
    except Exception:
        raise HTTPException(400, f"Invalid id: {v}")


def _ser(d: dict) -> dict:
    if not d:
        return d
    d = dict(d)
    d["id"] = str(d.pop("_id"))
    return d


def _now() -> str:
    return datetime.now(timezone.utc).isoformat()


def _slugify(s: str) -> str:
    s = re.sub(r"[^a-z0-9]+", "-", (s or "").lower()).strip("-")
    return s[:48] or f"module-{int(datetime.now().timestamp())}"


# =============== Constants — guardrails ===============
ALLOWED_FIELD_TYPES = {
    "text", "textarea", "number", "currency", "percent", "date", "datetime",
    "select", "multiselect", "switch", "phone", "email", "url", "user",
}

# Collections the AI may NEVER target — generated specs are forced to
# write to db.custom_records.<slug>, never to existing core collections.
FORBIDDEN_COLLECTIONS = {
    "users", "companies", "audit_log", "permissions", "backups", "roles",
    "licenses", "system_settings", "sessions", "rate_limits", "items",
    "parties", "invoices", "expenses",
}

DEFAULT_SETTINGS = {
    "enabled": False,                     # kill switch — Super Admin must opt in
    "allow_database_changes": True,       # whether AI may register new logical collections
    "require_approval": True,             # drafts → published only after admin click
    "auto_deploy": False,                 # reserved — currently never used
    "per_user_access": [],                # list of user emails allowed to USE the builder (empty = admins only)
}


# =============== Pydantic models ===============
class BuilderSettings(BaseModel):
    enabled: bool = False
    allow_database_changes: bool = True
    require_approval: bool = True
    auto_deploy: bool = False
    per_user_access: List[str] = []


class GenerateRequest(BaseModel):
    prompt: str = Field(..., min_length=10, max_length=4000)
    model: str = "claude-sonnet-4-6"


class CustomField(BaseModel):
    key: str
    label: str
    type: str
    required: bool = False
    placeholder: Optional[str] = ""
    options: Optional[List[str]] = None    # for select / multiselect


class ModuleSpec(BaseModel):
    name: str
    slug: str
    icon: Optional[str] = "box"
    color: Optional[str] = "#0C7C59"
    description: Optional[str] = ""
    fields: List[CustomField]
    list_columns: List[str] = []
    permissions: List[str] = ["view", "create", "edit", "delete"]


class RecordIn(BaseModel):
    """Generic record body — keys match the module's field definition."""
    data: Dict[str, Any] = {}


# =============== Super Admin settings ===============
@router.get("/settings")
async def get_settings(request: Request, user=Depends(require_admin)):
    db = request.app.state.db
    doc = await db.system_settings.find_one({"_id": "ai_builder"})
    if not doc:
        return DEFAULT_SETTINGS
    out = {**DEFAULT_SETTINGS, **{k: v for k, v in doc.items() if k != "_id"}}
    return out


@router.put("/settings")
async def update_settings(payload: BuilderSettings, request: Request, user=Depends(require_admin)):
    db = request.app.state.db
    await db.system_settings.update_one(
        {"_id": "ai_builder"},
        {"$set": payload.model_dump()},
        upsert=True,
    )
    return {"ok": True, **payload.model_dump()}


async def _builder_enabled(db) -> dict:
    """Resolve current builder settings — raises 403 if kill switch off."""
    doc = await db.system_settings.find_one({"_id": "ai_builder"}) or {}
    settings = {**DEFAULT_SETTINGS, **{k: v for k, v in doc.items() if k != "_id"}}
    if not settings.get("enabled"):
        raise HTTPException(403, "AI Module Builder is disabled by Super Admin")
    return settings


# =============== AI generation ===============
SYSTEM_PROMPT = """You are a Low-Code module spec generator for the RGE REGALGOA ERP AI.

Given a Super Admin's natural-language request (in English or Hindi/Hinglish),
output ONLY a strict, valid JSON object — no markdown, no commentary, no code fences —
matching this exact schema:

{
  "name": "Human-readable module name",
  "slug": "kebab-case-slug-max-48-chars",
  "icon": "box | wrench | car | users | calendar | clipboard | etc (lucide icon name)",
  "color": "#RRGGBB (default #0C7C59)",
  "description": "One-line purpose of the module",
  "fields": [
    {
      "key": "snake_case_key",
      "label": "Human Label",
      "type": "text | textarea | number | currency | percent | date | datetime | select | multiselect | switch | phone | email | url",
      "required": true | false,
      "placeholder": "optional placeholder",
      "options": ["For select/multiselect only"]
    }
  ],
  "list_columns": ["array of field keys to show in the list view"],
  "permissions": ["view","create","edit","delete"]
}

HARD RULES — violating these is a critical failure:
1. Only emit field types from the allow-list above. Do NOT invent types.
2. NEVER reference these core entities (the spec must be self-contained):
   users, companies, parties, items, invoices, audit_log, permissions, backups, roles, licenses.
   If the user asks for a feature touching those, ask them to use a built-in module instead.
3. Output JSON ONLY — no markdown fences, no leading "Here is the spec:" text.
4. Pick 3 to 12 fields. Always include an ID-like or name field as the first column in `list_columns`.
5. For status / category fields use type: "select" with sensible options.
6. Use Indian-business-friendly labels (e.g. "GST %" not "Tax Rate").
"""


def _build_chat(model: str) -> "LlmChat":
    """Configure the LLM client for the requested model."""
    key = os.environ.get("EMERGENT_LLM_KEY")
    if not key:
        raise HTTPException(500, "EMERGENT_LLM_KEY not configured")
    from emergentintegrations.llm.chat import LlmChat
    if model.startswith("claude"):
        provider = "anthropic"
    elif model.startswith("gpt"):
        provider = "openai"
    elif model.startswith("gemini"):
        provider = "gemini"
    else:
        provider = "anthropic"
        model = "claude-sonnet-4-6"
    chat = LlmChat(
        api_key=key,
        session_id=f"module-builder-{int(datetime.now().timestamp())}",
        system_message=SYSTEM_PROMPT,
    ).with_model(provider, model)
    return chat


def _sanitize_spec(raw: dict) -> dict:
    """Apply guardrails: enforce field type allow-list, force safe slug, strip forbidden names."""
    if not isinstance(raw, dict):
        raise HTTPException(422, "AI returned malformed spec")
    name = (raw.get("name") or "").strip()
    if not name:
        raise HTTPException(422, "AI spec missing 'name'")
    slug = _slugify(raw.get("slug") or name)
    if slug in FORBIDDEN_COLLECTIONS:
        raise HTTPException(422, f"Module slug '{slug}' conflicts with a core collection")
    fields_in = raw.get("fields") or []
    if not isinstance(fields_in, list) or not fields_in:
        raise HTTPException(422, "AI spec must contain at least one field")
    fields = []
    seen_keys = set()
    for f in fields_in[:12]:                              # cap at 12 fields
        if not isinstance(f, dict):
            continue
        key = _slugify(f.get("key") or f.get("label") or "field").replace("-", "_")
        if not key or key in seen_keys:
            continue
        seen_keys.add(key)
        ftype = (f.get("type") or "text").lower()
        if ftype not in ALLOWED_FIELD_TYPES:
            ftype = "text"
        fields.append({
            "key": key,
            "label": (f.get("label") or key.replace("_", " ").title()),
            "type": ftype,
            "required": bool(f.get("required", False)),
            "placeholder": f.get("placeholder") or "",
            "options": [str(o) for o in (f.get("options") or [])] if ftype in ("select", "multiselect") else None,
        })
    list_cols = [c for c in (raw.get("list_columns") or []) if c in seen_keys]
    if not list_cols:
        list_cols = [f["key"] for f in fields[:4]]
    return {
        "name": name[:80],
        "slug": slug,
        "icon": (raw.get("icon") or "box")[:40],
        "color": (raw.get("color") or "#0C7C59")[:9],
        "description": (raw.get("description") or "")[:240],
        "fields": fields,
        "list_columns": list_cols[:6],
        "permissions": ["view", "create", "edit", "delete"],
    }


@router.post("/generate")
async def generate_module(payload: GenerateRequest, request: Request, user=Depends(require_admin)):
    """Ask Claude to design a module spec from a natural-language prompt.

    Spec is saved as `draft` — needs explicit /publish to become usable.
    """
    db = request.app.state.db
    settings = await _builder_enabled(db)
    if user["email"] != os.environ.get("ADMIN_EMAIL") and user["email"] not in settings.get("per_user_access", []):
        # Admin allowed; other users must be explicitly granted access
        if user.get("role") != "admin":
            raise HTTPException(403, "Not authorised to use the AI Builder")

    # Call the LLM (non-streaming — we need the full JSON back)
    from emergentintegrations.llm.chat import UserMessage
    chat = _build_chat(payload.model)
    try:
        reply = await chat.send_message(UserMessage(text=payload.prompt))
    except Exception as e:
        raise HTTPException(502, f"LLM error: {e}")

    text = reply.strip() if isinstance(reply, str) else str(reply)
    # Be tolerant of accidental code fences
    text = re.sub(r"^```(?:json)?\s*|```\s*$", "", text.strip(), flags=re.MULTILINE)
    try:
        raw = json.loads(text)
    except json.JSONDecodeError:
        raise HTTPException(422, f"AI returned non-JSON: {text[:200]}")

    spec = _sanitize_spec(raw)
    spec["_id"] = ObjectId()
    spec["status"] = "draft" if settings.get("require_approval", True) else "published"
    spec["created_by"] = user["email"]
    spec["created_at"] = _now()
    spec["original_prompt"] = payload.prompt
    spec["model"] = payload.model
    # Ensure uniqueness — if slug exists, suffix it
    existing = await db.custom_modules.find_one({"slug": spec["slug"]})
    if existing:
        spec["slug"] = f"{spec['slug']}-{int(datetime.now().timestamp())}"
    await db.custom_modules.insert_one(spec)
    return _ser(spec)


# =============== Module CRUD (spec) ===============
@router.get("/modules")
async def list_modules(request: Request, user=Depends(get_current_user)):
    db = request.app.state.db
    docs = await db.custom_modules.find({}).sort("created_at", -1).to_list(500)
    return [_ser(d) for d in docs]


@router.get("/modules/{mid}")
async def get_module(mid: str, request: Request, user=Depends(get_current_user)):
    db = request.app.state.db
    doc = await db.custom_modules.find_one({"_id": _oid(mid)})
    if not doc:
        raise HTTPException(404, "Module not found")
    return _ser(doc)


@router.post("/modules/{mid}/publish")
async def publish_module(mid: str, request: Request, user=Depends(require_admin)):
    db = request.app.state.db
    res = await db.custom_modules.update_one(
        {"_id": _oid(mid)},
        {"$set": {"status": "published", "published_at": _now(), "published_by": user["email"]}},
    )
    if res.matched_count == 0:
        raise HTTPException(404, "Module not found")
    doc = await db.custom_modules.find_one({"_id": _oid(mid)})
    return _ser(doc)


@router.post("/modules/{mid}/unpublish")
async def unpublish_module(mid: str, request: Request, user=Depends(require_admin)):
    db = request.app.state.db
    await db.custom_modules.update_one({"_id": _oid(mid)}, {"$set": {"status": "draft"}})
    return {"ok": True}


@router.delete("/modules/{mid}")
async def delete_module(mid: str, request: Request, user=Depends(require_admin)):
    db = request.app.state.db
    doc = await db.custom_modules.find_one({"_id": _oid(mid)})
    if not doc:
        raise HTTPException(404, "Module not found")
    # Cascade — drop the records for this module
    await db.custom_records.delete_many({"module_slug": doc["slug"]})
    await db.custom_modules.delete_one({"_id": _oid(mid)})
    return {"ok": True}


# =============== Generic CRUD for records of a module ===============
async def _resolve_published_module(db, slug: str) -> dict:
    """Internal helper — returns the SAFE serialised spec of a published module.

    Already runs through `_ser()`, so no raw ObjectId leaks. Consumers may
    safely return this directly OR pluck individual keys from it.
    """
    raw = await db.custom_modules.find_one({"slug": slug, "status": "published"})
    if not raw:
        raise HTTPException(404, "Module not published")
    return _ser(raw)


@router.get("/records/{slug}")
async def list_records(slug: str, request: Request, limit: int = Query(200, le=2000), user=Depends(get_current_user)):
    db = request.app.state.db
    await _resolve_published_module(db, slug)
    docs = await db.custom_records.find({"module_slug": slug}).sort("created_at", -1).to_list(limit)
    return [_ser(d) for d in docs]


@router.post("/records/{slug}")
async def create_record(slug: str, payload: RecordIn, request: Request, user=Depends(get_current_user)):
    db = request.app.state.db
    spec = await _resolve_published_module(db, slug)
    # Validate required fields
    allowed_keys = {f["key"] for f in spec["fields"]}
    clean_data = {k: v for k, v in payload.data.items() if k in allowed_keys}
    for f in spec["fields"]:
        if f.get("required") and not clean_data.get(f["key"]):
            raise HTTPException(422, f"Field '{f['label']}' is required")
    doc = {
        "module_slug": slug,
        "data": clean_data,
        "created_by": user["email"],
        "created_at": _now(),
        "updated_at": _now(),
    }
    res = await db.custom_records.insert_one(doc)
    saved = await db.custom_records.find_one({"_id": res.inserted_id})
    return _ser(saved)


@router.put("/records/{slug}/{rid}")
async def update_record(slug: str, rid: str, payload: RecordIn, request: Request, user=Depends(get_current_user)):
    db = request.app.state.db
    spec = await _resolve_published_module(db, slug)
    allowed_keys = {f["key"] for f in spec["fields"]}
    clean_data = {k: v for k, v in payload.data.items() if k in allowed_keys}
    res = await db.custom_records.update_one(
        {"_id": _oid(rid), "module_slug": slug},
        {"$set": {"data": clean_data, "updated_at": _now(), "updated_by": user["email"]}},
    )
    if res.matched_count == 0:
        raise HTTPException(404, "Record not found")
    doc = await db.custom_records.find_one({"_id": _oid(rid)})
    return _ser(doc)


@router.delete("/records/{slug}/{rid}")
async def delete_record(slug: str, rid: str, request: Request, user=Depends(get_current_user)):
    db = request.app.state.db
    await _resolve_published_module(db, slug)
    res = await db.custom_records.delete_one({"_id": _oid(rid), "module_slug": slug})
    if res.deleted_count == 0:
        raise HTTPException(404, "Record not found")
    return {"ok": True}
