"""Luthier Physics tools — build management, engineering calculations, knowledge retrieval."""

import json
import math
import os
import shutil
from datetime import date, datetime, timedelta

from mentorr.helpers import (
    check_inventory,
    cosine_similarity,
    get_embedding,
    get_openai_client,
    get_package_data_path,
    get_user_data_dir,
    load_physics_db,
    load_template,
    build_library_index,
)


def _projects_dir():
    return get_user_data_dir() / "projects"


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


def _inventory_path():
    return get_user_data_dir() / "processed" / "master_inventory.csv"


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")


def register(mcp):
    """Register all Luthier Physics tools on the given FastMCP instance."""

    @mcp.tool()
    def register_project(name: str, type: str = "luthier", deadline: str = "", priority: str = "Medium") -> str:
        """
        Adds a project to the Master Schedule.
        type: 'luthier' (uses electric_6_string template) or 'music' (uses music_production template).
        deadline: 'YYYY-MM-DD'
        """
        schedule = _load_schedule()

        if any(p["name"] == name for p in schedule["projects"]):
            return f"Project '{name}' is already registered."

        template_name = "electric_6_string" if type == "luthier" else "music_production"
        start_build(name, template_name)

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

    @mcp.tool()
    def generate_work_day(available_hours: int = 8) -> str:
        """
        Generates a daily schedule based on active projects, deadlines, and inventory.
        """
        schedule = _load_schedule()
        if not schedule["projects"]:
            return "No active projects. Use 'register_project' to start one."

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

        plan = f"--- Work Day Plan ({available_hours} Hours) ---\n"
        hours_filled = 0

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

            p_name = project["name"]
            checklist_path = _projects_dir() / f"{p_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:
                step_name = next_step["step"]
                est_hours = next_step.get("hours", 2)
                materials = next_step.get("materials", [])

                missing_mats = [mat for mat in materials if not check_inventory(mat)]

                if missing_mats:
                    plan += f"[SKIP] {p_name}: '{step_name}' blocked. Missing: {', '.join(missing_mats)}\n"
                    continue

                allocated = min(est_hours, available_hours - hours_filled)
                plan += f"- {p_name}: {step_name} ({allocated} hrs)\n"
                if allocated < est_hours:
                    plan += "  (Partial work today)\n"
                hours_filled += allocated

        if hours_filled < available_hours:
            plan += f"\n[Free Time] {available_hours - hours_filled} hours remaining.\n"

        return plan

    @mcp.tool()
    def forecast_materials() -> str:
        """
        Checks all active projects and lists materials needed for UPCOMING steps.
        Helps you buy before you get blocked.
        """
        schedule = _load_schedule()
        report = "--- Material Forecast ---\n"

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

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

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

            needed = []
            for step in pending_steps:
                for mat in step.get("materials", []):
                    if not check_inventory(mat):
                        needed.append(f"{mat} (for {step['step']})")

            if needed:
                report += f"Project {p_name}:\n"
                for n in needed:
                    report += f"  [ ] Buy {n}\n"

        if "Buy" not in report:
            report += "All upcoming materials appear to be in stock."

        return report

    @mcp.tool()
    def start_build(project_name: str, template: str = "electric_6_string") -> str:
        """
        Initializes a new guitar build project with a checklist.
        project_name: e.g., "Dragon-1"
        template: "electric_6_string" (default)
        """
        projects_dir = _projects_dir()
        projects_dir.mkdir(parents=True, exist_ok=True)
        project_file = projects_dir / f"{project_name}_checklist.json"

        if project_file.exists():
            return f"Project '{project_name}' already exists."

        template_data = load_template(template)
        if template_data is None:
            return f"Template '{template}' not found. Available: electric_6_string, music_production"

        project_file.write_text(json.dumps(template_data, indent=4), encoding="utf-8")
        return f"Started build '{project_name}' using {template} template. Use 'get_build_status' to view."

    @mcp.tool()
    def get_build_status(project_name: str) -> str:
        """Returns the current todo list for a build."""
        path = _projects_dir() / f"{project_name}_checklist.json"
        if not path.exists():
            return f"Build '{project_name}' not found."

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

        output = f"--- Status: {project_name} ---\n"
        for i, s in enumerate(steps):
            mark = "[x]" if s["status"] == "Done" else "[ ]"
            output += f"{i+1}. {mark} {s['step']} ({s['status']})\n"
            if s.get("notes"):
                output += f"    Note: {s['notes']}\n"

        done = sum(1 for s in steps if s["status"] == "Done")
        total = len(steps)
        pct = int(done / total * 100) if total > 0 else 0
        output += f"\nProgress: {done}/{total} ({pct}%)"
        return output

    @mcp.tool()
    def update_build_step(project_name: str, step_number: int, status: str = "Done", note: str = "") -> str:
        """
        Updates a specific step in the build checklist.
        step_number: The number from the list (1-based index).
        status: "Done", "In Progress", "Pending".
        """
        path = _projects_dir() / f"{project_name}_checklist.json"
        if not path.exists():
            return f"Build '{project_name}' not found."

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

        if step_number < 1 or step_number > len(steps):
            return "Invalid step number."

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

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

    @mcp.tool()
    def customize_build(project_name: str, action: str, step_name: str, notes: str = "") -> str:
        """
        Adds a new custom step to the build.
        action: "add" (currently only add is supported for simplicity)
        step_name: Title of the new step
        """
        path = _projects_dir() / f"{project_name}_checklist.json"
        if not path.exists():
            return f"Build '{project_name}' not found."

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

        if action == "add":
            steps.append({"step": step_name, "status": "Pending", "notes": notes})

        path.write_text(json.dumps(steps, indent=4), encoding="utf-8")
        return f"Added step '{step_name}' to {project_name}."

    @mcp.tool()
    def consult_library(query: str) -> str:
        """Hybrid Semantic Search for luthier knowledge.
        Uses OpenAI Embeddings to find conceptually related text, even if keywords don't match exactly.
        """
        if get_openai_client() is None:
            return "OpenAI API key not configured. Set OPENAI_API_KEY to enable semantic search."

        build_library_index()

        index_path = get_user_data_dir() / "library_index.json"
        if not index_path.exists():
            return "Library is empty or index could not be built."

        index = json.loads(index_path.read_text(encoding="utf-8"))
        query_vec = get_embedding(query)
        if not query_vec:
            return "Could not generate embedding for query."

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

        for filename, data in index.items():
            for chunk in data["chunks"]:
                text = chunk["text"]
                vec = chunk["vector"]

                score = cosine_similarity(query_vec, vec)
                keyword_matches = sum(1 for k in query_keywords if k in text.lower())
                if keyword_matches > 0:
                    score += keyword_matches * 0.05

                if score > 0.35:
                    results.append((score, filename, text))

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

        output = f"--- Library Search: '{query}' ---"
        for score, fname, text in results[:4]:
            output += f"\n[Score: {score:.2f} | Source: {fname}]\n{text[:500]}...\n"

        if not results:
            output += "\nNo relevant results found."

        return output

    @mcp.tool()
    def save_project_specs(name: str, specs: str) -> str:
        """Saves measurements and design specs for a specific 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(specs, encoding="utf-8")
        return f"Project '{name}' specs saved."

    @mcp.tool()
    def get_project_specs(name: str) -> str:
        """Retrieves saved measurements for a build."""
        filename = f"{name.replace(' ', '_').lower()}.txt"
        path = _projects_dir() / filename
        if not path.exists():
            return f"Project '{name}' not found."
        return f"--- Project: {name} ---\n\n" + path.read_text(encoding="utf-8")

    @mcp.tool()
    def get_wood_specs(species: str) -> str:
        """Look up physical properties of a wood species."""
        db = load_physics_db()
        matches = [w for w in db if species.lower() in w["species"].lower()]
        if not matches:
            return f"No data found for '{species}'."
        result = ""
        for wood in matches:
            result += f"--- {wood['species']} ({wood['category']}) ---\n"
            result += f"  Density: {wood['density_kg_m3']} kg/m3\n"
            result += f"  Janka: {wood['janka_hardness_lbf']} lbf\n"
            result += f"  MOE: {wood['moe_gpa']} GPa\n\n"
        return result

    @mcp.tool()
    def calculate_fret_layout(scale_length_inches: float, number_of_frets: int = 22) -> str:
        """Generates fret slotting table."""
        output = (
            f"Fret Layout for {scale_length_inches}\" Scale:\n"
            "Fret | Dist from Nut (in) | Dist from Nut (mm)\n"
            "-----|--------------------|-------------------\n"
        )
        for i in range(1, number_of_frets + 1):
            dist = scale_length_inches * (1 - (1 / (2 ** (i / 12))))
            output += f"{i:4} | {dist:18.4f} | {dist * 25.4:17.2f}\n"
        return output

    @mcp.tool()
    def calculate_string_tension(scale_length: float, set_type: str = "light", tuning: str = "standard") -> str:
        """Calculates total string tension."""
        NOTE_FREQUENCIES = {
            "E2": 82.41, "A2": 110.0, "D3": 146.83,
            "G3": 196.0, "B3": 246.94, "E4": 329.63,
        }
        UNIT_WEIGHTS = {
            "0.012": 0.00003196, "0.016": 0.00005677, "0.024": 0.000125,
            "0.032": 0.000222, "0.042": 0.000377, "0.053": 0.000595,
        }
        gauges = ["0.012", "0.016", "0.024", "0.032", "0.042", "0.053"]
        pitches = ["E4", "B3", "G3", "D3", "A2", "E2"]

        total = 0
        res = f"Tension ({scale_length}\", {set_type}):\n"
        for i, g in enumerate(gauges):
            f = NOTE_FREQUENCIES.get(pitches[i], 0)
            uw = UNIT_WEIGHTS.get(g, 0)
            t = (uw * (2 * scale_length * f) ** 2) / 386.4
            total += t
            res += f"  String {i+1}: {t:.2f} lbs\n"
        res += f"Total: {total:.2f} lbs"
        return res

    @mcp.tool()
    def calculate_saddle_placement(scale_length: float, string_type: str = "steel") -> str:
        """
        Calculates the exact location for the saddle slot to ensure proper intonation (Compensation).
        Args:
            scale_length: The theoretical scale length in inches (e.g. 25.4).
            string_type: 'steel' or 'nylon'. Steel requires more compensation.
        """
        if string_type.lower() == "nylon":
            comp_treble = 0.04
            comp_bass = 0.08
        else:
            comp_treble = 0.06
            comp_bass = 0.15

        loc_treble = scale_length + comp_treble
        loc_bass = scale_length + comp_bass

        return (
            f"Saddle Placement for {scale_length}\" {string_type.title()}:\n"
            f"  Treble (High E): {loc_treble:.4f}\" from Nut\n"
            f"  Bass (Low E):    {loc_bass:.4f}\" from Nut\n"
        )

    @mcp.tool()
    def calculate_neck_angle_geometry(
        desired_action_12th: float = 0.090,
        saddle_protrusion: float = 0.125,
        fret_height: float = 0.040,
    ) -> str:
        """Calculates target bridge thickness."""
        height_above_frets = desired_action_12th * 2
        total_height = height_above_frets + fret_height
        bridge_thickness = total_height - saddle_protrusion
        return f"Target Bridge Thickness: {bridge_thickness:.3f}\" ({bridge_thickness * 25.4:.2f} mm)"
