"""MENTORR API Server — FastAPI wrapper for Custom GPT Actions.

Exposes the Tier 2 capabilities (build management, inventory, semantic search)
as HTTP endpoints that a ChatGPT Custom GPT can call via Actions.

Run locally:
    uvicorn api_server:app --reload --port 8000

Deploy:
    fly launch  (uses Dockerfile + fly.toml)
"""

import csv
import json
import os
from pathlib import Path
from typing import Optional

from fastapi import FastAPI, HTTPException, Query, Security
from fastapi.responses import HTMLResponse
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from pydantic import BaseModel

# ---------------------------------------------------------------------------
# App Setup
# ---------------------------------------------------------------------------

app = FastAPI(
    title="MENTORR API",
    version="0.1.0",
    description="Lutherie build management, inventory, and knowledge API for the MENTORR Custom GPT.",
)

security = HTTPBearer(auto_error=False)

API_KEY = os.environ.get("MENTORR_API_KEY", "")


def verify_token(credentials: Optional[HTTPAuthorizationCredentials] = Security(security)):
    """Verify the Bearer token matches the configured API key."""
    if not API_KEY:
        return  # No key configured = open access (dev mode)
    if credentials is None or credentials.credentials != API_KEY:
        raise HTTPException(status_code=401, detail="Invalid API key")


# ---------------------------------------------------------------------------
# Data Directory (same layout as ~/.mentorr/)
# ---------------------------------------------------------------------------

DATA_DIR = Path(os.environ.get("MENTORR_DATA_DIR", Path.home() / ".mentorr"))


def _ensure_dirs():
    for subdir in ["projects", "processed", "raw", "lab_inbox", "lab_processed"]:
        (DATA_DIR / subdir).mkdir(parents=True, exist_ok=True)


_ensure_dirs()


def _projects_dir():
    return DATA_DIR / "projects"


def _schedule_path():
    return _projects_dir() / "studio_schedule.json"


def _load_schedule():
    path = _schedule_path()
    if path.exists():
        return json.loads(path.read_text(encoding="utf-8"))
    return {"projects": []}


def _save_schedule(data):
    path = _schedule_path()
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(data, indent=4), encoding="utf-8")


# ---------------------------------------------------------------------------
# Templates (bundled with the API server)
# ---------------------------------------------------------------------------

TEMPLATES_DIR = Path(__file__).parent / "knowledge"


def _load_template(template_name: str):
    path = TEMPLATES_DIR / f"{template_name}_template.json"
    if path.exists():
        return json.loads(path.read_text(encoding="utf-8"))
    return None


# ---------------------------------------------------------------------------
# Models
# ---------------------------------------------------------------------------

class StartBuildRequest(BaseModel):
    template: str = "electric_6_string"


class UpdateStepRequest(BaseModel):
    status: str = "Done"
    note: str = ""


class AddStepRequest(BaseModel):
    step_name: str
    notes: str = ""


class SaveSpecsRequest(BaseModel):
    specs: str


class RegisterProjectRequest(BaseModel):
    name: str
    type: str = "luthier"
    deadline: str = ""
    priority: str = "Medium"


# ---------------------------------------------------------------------------
# Build Management Endpoints
# ---------------------------------------------------------------------------

@app.post("/builds/{name}", dependencies=[Security(verify_token)])
def start_build(name: str, body: StartBuildRequest):
    """Initialize a new guitar build project with a checklist."""
    projects_dir = _projects_dir()
    projects_dir.mkdir(parents=True, exist_ok=True)
    project_file = projects_dir / f"{name}_checklist.json"

    if project_file.exists():
        raise HTTPException(status_code=409, detail=f"Project '{name}' already exists.")

    template_data = _load_template(body.template)
    if template_data is None:
        raise HTTPException(
            status_code=404,
            detail=f"Template '{body.template}' not found. Available: electric_6_string, music_production",
        )

    project_file.write_text(json.dumps(template_data, indent=4), encoding="utf-8")
    return {"message": f"Started build '{name}' using {body.template} template.", "steps": len(template_data)}


@app.get("/builds/{name}/status", dependencies=[Security(verify_token)])
def get_build_status(name: str):
    """View checklist progress for a build."""
    path = _projects_dir() / f"{name}_checklist.json"
    if not path.exists():
        raise HTTPException(status_code=404, detail=f"Build '{name}' not found.")

    steps = json.loads(path.read_text(encoding="utf-8"))
    done = sum(1 for s in steps if s["status"] == "Done")
    total = len(steps)

    return {
        "project": name,
        "steps": steps,
        "done": done,
        "total": total,
        "percent": int(done / total * 100) if total > 0 else 0,
    }


@app.patch("/builds/{name}/steps/{step_number}", dependencies=[Security(verify_token)])
def update_build_step(name: str, step_number: int, body: UpdateStepRequest):
    """Mark a build step as Done/In Progress/Pending."""
    path = _projects_dir() / f"{name}_checklist.json"
    if not path.exists():
        raise HTTPException(status_code=404, detail=f"Build '{name}' not found.")

    steps = json.loads(path.read_text(encoding="utf-8"))

    if step_number < 1 or step_number > len(steps):
        raise HTTPException(status_code=400, detail="Invalid step number.")

    idx = step_number - 1
    steps[idx]["status"] = body.status
    if body.note:
        steps[idx]["notes"] = body.note

    path.write_text(json.dumps(steps, indent=4), encoding="utf-8")
    return {"message": f"Updated Step {step_number}: {steps[idx]['step']} -> {body.status}"}


@app.post("/builds/{name}/steps", dependencies=[Security(verify_token)])
def add_build_step(name: str, body: AddStepRequest):
    """Add a custom step to a build checklist."""
    path = _projects_dir() / f"{name}_checklist.json"
    if not path.exists():
        raise HTTPException(status_code=404, detail=f"Build '{name}' not found.")

    steps = json.loads(path.read_text(encoding="utf-8"))
    steps.append({"step": body.step_name, "status": "Pending", "notes": body.notes})
    path.write_text(json.dumps(steps, indent=4), encoding="utf-8")
    return {"message": f"Added step '{body.step_name}' to {name}."}


@app.get("/builds/{name}/specs", dependencies=[Security(verify_token)])
def get_project_specs(name: str):
    """Retrieve saved measurements for a build."""
    filename = f"{name.replace(' ', '_').lower()}.txt"
    path = _projects_dir() / filename
    if not path.exists():
        raise HTTPException(status_code=404, detail=f"Specs for '{name}' not found.")
    return {"project": name, "specs": path.read_text(encoding="utf-8")}


@app.put("/builds/{name}/specs", dependencies=[Security(verify_token)])
def save_project_specs(name: str, body: SaveSpecsRequest):
    """Save measurements and design specs for a build."""
    projects_dir = _projects_dir()
    projects_dir.mkdir(parents=True, exist_ok=True)
    filename = f"{name.replace(' ', '_').lower()}.txt"
    path = projects_dir / filename
    path.write_text(body.specs, encoding="utf-8")
    return {"message": f"Specs for '{name}' saved."}


# ---------------------------------------------------------------------------
# Schedule Endpoints
# ---------------------------------------------------------------------------

@app.post("/schedule/projects", dependencies=[Security(verify_token)])
def register_project(body: RegisterProjectRequest):
    """Add a project to the Master Schedule."""
    schedule = _load_schedule()

    if any(p["name"] == body.name for p in schedule["projects"]):
        raise HTTPException(status_code=409, detail=f"Project '{body.name}' already registered.")

    # Also create the build checklist
    template_name = "electric_6_string" if body.type == "luthier" else "music_production"
    project_file = _projects_dir() / f"{body.name}_checklist.json"
    if not project_file.exists():
        template_data = _load_template(template_name)
        if template_data:
            project_file.write_text(json.dumps(template_data, indent=4), encoding="utf-8")

    schedule["projects"].append({
        "name": body.name,
        "type": body.type,
        "deadline": body.deadline,
        "priority": body.priority,
        "status": "Active",
    })
    _save_schedule(schedule)
    return {"message": f"Registered '{body.name}' ({body.type}) due {body.deadline}."}


@app.get("/schedule/workday", dependencies=[Security(verify_token)])
def generate_work_day(available_hours: int = Query(default=8)):
    """Generate a prioritized daily schedule across active projects."""
    schedule = _load_schedule()
    if not schedule["projects"]:
        return {"plan": "No active projects. Register a project first.", "tasks": []}

    priority_map = {"High": 1, "Medium": 2, "Low": 3}
    active = [p for p in schedule["projects"] if p["status"] == "Active"]
    active.sort(key=lambda x: priority_map.get(x.get("priority", "Medium"), 2))

    tasks = []
    hours_filled = 0

    for project in active:
        if hours_filled >= available_hours:
            break

        checklist_path = _projects_dir() / f"{project['name']}_checklist.json"
        if not checklist_path.exists():
            continue

        steps = json.loads(checklist_path.read_text(encoding="utf-8"))
        next_step = next((s for s in steps if s["status"] == "Pending"), None)

        if next_step:
            est_hours = next_step.get("hours", 2)
            allocated = min(est_hours, available_hours - hours_filled)
            tasks.append({
                "project": project["name"],
                "step": next_step["step"],
                "hours": allocated,
                "partial": allocated < est_hours,
            })
            hours_filled += allocated

    return {
        "available_hours": available_hours,
        "hours_scheduled": hours_filled,
        "hours_free": available_hours - hours_filled,
        "tasks": tasks,
    }


@app.get("/schedule/materials", dependencies=[Security(verify_token)])
def forecast_materials():
    """List materials needed for upcoming steps across all active projects."""
    schedule = _load_schedule()
    forecast = []

    for project in schedule["projects"]:
        if project["status"] != "Active":
            continue

        checklist_path = _projects_dir() / f"{project['name']}_checklist.json"
        if not checklist_path.exists():
            continue

        steps = json.loads(checklist_path.read_text(encoding="utf-8"))
        pending = [s for s in steps if s["status"] == "Pending"][:3]

        for step in pending:
            for mat in step.get("materials", []):
                forecast.append({
                    "project": project["name"],
                    "step": step["step"],
                    "material": mat,
                })

    return {"materials_needed": forecast}


# ---------------------------------------------------------------------------
# Inventory Endpoints
# ---------------------------------------------------------------------------

@app.get("/inventory/search", dependencies=[Security(verify_token)])
def search_inventory(query: str = Query(...), limit: int = Query(default=10)):
    """Full-text search across the master inventory."""
    master_path = DATA_DIR / "processed" / "master_inventory.csv"
    if not master_path.exists():
        return {"matches": [], "message": "Master inventory not found. Run an update first."}

    matches = []
    query_lower = query.lower()

    with open(str(master_path), "r", encoding="utf-8") as f:
        reader = csv.DictReader(f)
        for row in reader:
            if row.get("status") != "Active":
                continue
            content = f"{row.get('name', '')} {row.get('species', '')} {row.get('raw_attributes', '')}".lower()
            if query_lower in content:
                matches.append({
                    "seller": row.get("seller", ""),
                    "name": row.get("name", ""),
                    "species": row.get("species", ""),
                    "price": row.get("price", ""),
                    "link": row.get("link", ""),
                })
                if len(matches) >= limit:
                    break

    return {"query": query, "count": len(matches), "matches": matches}


@app.get("/inventory/stats", dependencies=[Security(verify_token)])
def get_inventory_stats():
    """Summary statistics of the master inventory."""
    master_path = DATA_DIR / "processed" / "master_inventory.csv"
    if not master_path.exists():
        return {"message": "Master inventory not found. Run an update first."}

    seller_counts = {}
    species_counts = {}
    total = 0

    with open(str(master_path), "r", encoding="utf-8") as f:
        reader = csv.DictReader(f)
        for row in reader:
            if row.get("status") == "Active":
                total += 1
                seller = row.get("seller", "Unknown")
                species = row.get("species", "Unknown")
                seller_counts[seller] = seller_counts.get(seller, 0) + 1
                species_counts[species] = species_counts.get(species, 0) + 1

    top_species = sorted(species_counts.items(), key=lambda x: x[1], reverse=True)[:10]

    return {
        "total_active": total,
        "by_seller": seller_counts,
        "top_species": [{"species": s, "count": c} for s, c in top_species],
    }


@app.post("/inventory/update/{source}", dependencies=[Security(verify_token)])
def update_inventory(source: str):
    """Trigger a scraper and sync inventory. Source: shelley, exotic, stewmac."""
    try:
        from mentorr.pipeline import sync_master

        data_dir = str(DATA_DIR)

        if source == "shelley":
            from mentorr.pipeline import scrape_shelley
            scrape_out = scrape_shelley.run(data_dir, limit=10000)
        elif source == "exotic":
            from mentorr.pipeline import scrape_exoticwoodzone
            scrape_out = scrape_exoticwoodzone.run(data_dir, limit=5000)
        elif source == "stewmac":
            from mentorr.pipeline import scrape_stewmac
            scrape_out = scrape_stewmac.run(data_dir)
        else:
            raise HTTPException(status_code=400, detail=f"Unknown source '{source}'. Use: shelley, exotic, stewmac")

        sync_out = sync_master.run(data_dir)

        return {"source": source, "scrape": scrape_out, "sync": sync_out}

    except ImportError as e:
        raise HTTPException(status_code=500, detail=f"Pipeline module not available: {e}")


# ---------------------------------------------------------------------------
# Library (Semantic Search)
# ---------------------------------------------------------------------------

@app.get("/library/search", dependencies=[Security(verify_token)])
def search_library(query: str = Query(...)):
    """Semantic search across the luthier knowledge base."""
    try:
        from mentorr.helpers import get_embedding, cosine_similarity, build_library_index
    except ImportError:
        raise HTTPException(status_code=500, detail="MENTORR package not installed on server.")

    try:
        from openai import OpenAI
    except ImportError:
        raise HTTPException(status_code=503, detail="OpenAI package not installed. Semantic search unavailable.")

    if not os.environ.get("OPENAI_API_KEY"):
        raise HTTPException(status_code=503, detail="OPENAI_API_KEY not set. Semantic search unavailable.")

    build_library_index()

    index_path = DATA_DIR / "library_index.json"
    if not index_path.exists():
        return {"query": query, "results": [], "message": "Library index is empty."}

    index = json.loads(index_path.read_text(encoding="utf-8"))
    query_vec = get_embedding(query)
    if not query_vec:
        raise HTTPException(status_code=500, detail="Failed to generate embedding for query.")

    query_keywords = query.lower().split()
    results = []

    for filename, data in index.items():
        for chunk in data["chunks"]:
            score = cosine_similarity(query_vec, chunk["vector"])
            keyword_matches = sum(1 for k in query_keywords if k in chunk["text"].lower())
            if keyword_matches > 0:
                score += keyword_matches * 0.05
            if score > 0.35:
                results.append({"score": round(score, 3), "source": filename, "text": chunk["text"][:500]})

    results.sort(key=lambda x: x["score"], reverse=True)

    return {"query": query, "results": results[:4]}


# ---------------------------------------------------------------------------
# Health Check
# ---------------------------------------------------------------------------

@app.get("/health")
def health():
    return {"status": "ok", "version": "0.1.0"}


@app.get("/privacy", response_class=HTMLResponse)
def privacy_policy():
    return """<!DOCTYPE html>
<html><head><title>MENTORR Privacy Policy</title></head>
<body style="font-family:system-ui;max-width:700px;margin:40px auto;padding:0 20px;line-height:1.6">
<h1>MENTORR Privacy Policy</h1>
<p><strong>Effective:</strong> February 2026</p>
<h2>What We Collect</h2>
<p>MENTORR stores guitar build checklists, project specs, and inventory data that you
create through the API. This data is stored on a private server and is only accessible
with your API key.</p>
<h2>What We Don't Collect</h2>
<ul>
<li>No personal information (name, email, address)</li>
<li>No analytics or tracking</li>
<li>No cookies</li>
<li>No third-party data sharing</li>
</ul>
<h2>Data Storage</h2>
<p>Build data is stored on a Railway-hosted server with persistent volume storage.
Data is not shared with any third parties.</p>
<h2>Data Deletion</h2>
<p>Contact the administrator to request deletion of any stored data.</p>
<h2>Contact</h2>
<p>For questions, reach out to the project maintainer at Shelley Guitar.</p>
</body></html>"""
