#!/usr/bin/env python
"""
Flask server for Path Navigator with OSC support.
"""

from flask import Flask, jsonify, request, send_from_directory
import json
import os
from pathlib import Path

# Add parent directory to path for imports
import sys

sys.path.insert(0, str(Path(__file__).parent.parent))

from src.pitch import Pitch
from src.osc_sender import OSCSender
from fractions import Fraction

app = Flask(__name__)

# Path to data files
DATA_DIR = Path(__file__).parent.parent / "output"
DATA_FILE = "output_chords.json"  # default file

# State
current_index = 0
chords = []
dims = (2, 3, 5, 7)

# OSC settings
fundamental = 110.0
osc_sender = OSCSender(ip="127.0.0.1", port=57120, fundamental=fundamental)


def get_chords_file():
    return DATA_DIR / DATA_FILE


def load_chords():
    global chords, current_index
    chords_file = get_chords_file()
    if chords_file.exists():
        with open(chords_file) as f:
            data = json.load(f)
            chords = data.get("chords", [])
        current_index = 0
    else:
        chords = []


load_chords()


def parse_fraction(frac_str):
    """Parse a fraction string to float."""
    if isinstance(frac_str, (int, float)):
        return float(frac_str)
    if "/" in frac_str:
        num, den = frac_str.split("/")
        return int(num) / int(den)
    return float(frac_str)


def calculate_cents(fraction_str):
    """Calculate cents from fraction string."""
    fr = parse_fraction(fraction_str)
    if fr <= 0:
        return 0
    return 1200 * (
        float(fr).bit_length()
        - 1
        + (fr / (1 << (fr.bit_length() - 1)) - 1) / 0.6931471805599453
        if hasattr(fr, "bit_length")
        else 1200 * (float(fr).bit_length() - 1)
    )
    # Simpler: use log2
    import math

    return 1200 * math.log2(fr)


def calculate_graph(chord):
    """Calculate nodes and edges for a chord using pitch logic."""
    if not chord:
        return {"nodes": [], "edges": []}

    # Calculate cents for each pitch
    import math

    nodes = []
    cents_list = []

    for i, pitch in enumerate(chord):
        fr = parse_fraction(pitch.get("fraction", "1"))
        cents = 1200 * math.log2(fr) if fr > 0 else 0
        cents_list.append(cents)

        nodes.append(
            {
                "id": i,
                "cents": round(cents),
                "fraction": pitch.get("fraction", "1"),
                "hs_array": pitch.get("hs_array", []),
            }
        )

    # Find edges: differ by ±1 in exactly one dimension (ignoring dim 0)
    edges = []
    for i in range(len(chord)):
        for j in range(i + 1, len(chord)):
            hs1 = chord[i].get("hs_array", [])
            hs2 = chord[j].get("hs_array", [])

            if not hs1 or not hs2:
                continue

            # Count differences in dims 1, 2, 3
            diff_count = 0
            diff_dim = -1

            for d in range(1, len(hs1)):
                diff = hs2[d] - hs1[d]
                if abs(diff) == 1:
                    diff_count += 1
                    diff_dim = d
                elif diff != 0:
                    break  # diff > 1 in this dimension
            else:
                # Check if exactly one dimension differs
                if diff_count == 1 and diff_dim > 0:
                    # Calculate frequency ratio from pitch difference
                    # diff = hs1 - hs2 gives direction
                    # Convert to fraction
                    diff_hs = [hs1[d] - hs2[d] for d in range(len(hs1))]

                    numerator = 1
                    denominator = 1
                    for d_idx, d in enumerate(dims):
                        exp = diff_hs[d_idx]
                        if exp > 0:
                            numerator *= d**exp
                        elif exp < 0:
                            denominator *= d ** (-exp)

                    ratio = (
                        f"{numerator}/{denominator}"
                        if denominator > 1
                        else str(numerator)
                    )

                    edges.append(
                        {"source": i, "target": j, "ratio": ratio, "dim": diff_dim}
                    )

    return {"nodes": nodes, "edges": edges}


@app.route("/")
def index():
    return send_from_directory(".", "path_navigator.html")


@app.route("/<path:filename>")
def serve_static(filename):
    return send_from_directory(".", filename)


@app.route("/api/files")
def list_files():
    """List available output files."""
    files = []
    if DATA_DIR.exists():
        for f in DATA_DIR.iterdir():
            if f.is_file() and f.suffix == ".json" and "chords" in f.name:
                files.append(f.name)
    return jsonify({"files": sorted(files)})


@app.route("/api/set-file/<filename>", methods=["POST"])
def set_data_file(filename):
    global DATA_FILE, current_index
    DATA_FILE = filename
    current_index = 0
    load_chords()
    return jsonify({"file": DATA_FILE, "loaded": len(chords), "index": current_index})


@app.route("/api/chords")
def get_chords():
    return jsonify({"chords": chords, "total": len(chords)})


@app.route("/api/current")
def get_current():
    if not chords:
        return jsonify({"error": "No chords loaded"}), 404

    prev_idx = current_index - 1 if current_index > 0 else None
    next_idx = current_index + 1 if current_index < len(chords) - 1 else None

    return jsonify(
        {
            "index": current_index,
            "total": len(chords),
            "prev": chords[prev_idx] if prev_idx is not None else None,
            "current": chords[current_index],
            "next": chords[next_idx] if next_idx is not None else None,
            "has_prev": prev_idx is not None,
            "has_next": next_idx is not None,
        }
    )


@app.route("/api/graph/<int:index>")
def get_graph(index):
    """Get computed graph for prev/current/next at given index."""
    if not chords:
        return jsonify({"error": "No chords loaded"}), 404

    if not (0 <= index < len(chords)):
        return jsonify({"error": "Invalid index"}), 400

    prev_idx = index - 1 if index > 0 else None
    next_idx = index + 1 if index < len(chords) - 1 else None

    return jsonify(
        {
            "index": index,
            "total": len(chords),
            "prev": calculate_graph(chords[prev_idx]) if prev_idx is not None else None,
            "current": calculate_graph(chords[index]),
            "next": calculate_graph(chords[next_idx]) if next_idx is not None else None,
            "has_prev": prev_idx is not None,
            "has_next": next_idx is not None,
        }
    )


@app.route("/api/all-graphs")
def get_all_graphs():
    """Get computed graph for ALL chords at once for single-canvas rendering."""
    if not chords:
        return jsonify({"error": "No chords loaded"}), 404

    all_graphs = []
    for i, chord in enumerate(chords):
        graph = calculate_graph(chord)
        graph["index"] = i
        all_graphs.append(graph)

    return jsonify(
        {
            "total": len(chords),
            "graphs": all_graphs,
        }
    )


@app.route("/api/navigate", methods=["POST"])
def navigate():
    global current_index
    data = request.json
    direction = data.get("direction", "next")

    if direction == "prev" and current_index > 0:
        current_index -= 1
    elif direction == "next" and current_index < len(chords) - 1:
        current_index += 1

    return jsonify({"index": current_index})


@app.route("/api/goto/<int:index>")
def goto(index):
    global current_index
    if 0 <= index < len(chords):
        current_index = index
        return jsonify({"index": current_index})
    return jsonify({"error": "Invalid index"}), 400


@app.route("/api/reload", methods=["POST"])
def reload():
    load_chords()
    return jsonify({"loaded": len(chords), "index": current_index})


@app.route("/api/set-fundamental", methods=["POST"])
def set_fundamental():
    """Set the fundamental frequency for OSC playback."""
    global fundamental, osc_sender
    data = request.json
    fundamental = data.get("fundamental", 110.0)
    osc_sender = OSCSender(ip="127.0.0.1", port=57120, fundamental=fundamental)
    return jsonify({"fundamental": fundamental})


@app.route("/api/play-freq", methods=["POST"])
def play_freq():
    """Play a single frequency for a node."""
    data = request.json
    chord_index = data.get("chordIndex")
    node_index = data.get("nodeIndex")

    if chord_index < 0 or chord_index >= len(chords):
        return jsonify({"error": "Invalid chord index"}), 400

    chord = chords[chord_index]
    if node_index < 0 or node_index >= len(chord):
        return jsonify({"error": "Invalid node index"}), 400

    pitch = chord[node_index]
    fraction = Fraction(pitch.get("fraction", "1"))
    frequency = fundamental * float(fraction)
    voice = node_index + 1  # 1-indexed

    osc_sender.send_single(frequency, voice)

    return jsonify(
        {
            "frequency": frequency,
            "voice": voice,
            "fundamental": fundamental,
            "fraction": pitch.get("fraction", "1"),
        }
    )


@app.route("/api/play-siren", methods=["POST"])
def play_siren():
    """Play a single frequency for a node on the siren."""
    data = request.json
    chord_index = data.get("chordIndex")
    node_index = data.get("nodeIndex")

    if chord_index < 0 or chord_index >= len(chords):
        return jsonify({"error": "Invalid chord index"}), 400

    chord = chords[chord_index]
    if node_index < 0 or node_index >= len(chord):
        return jsonify({"error": "Invalid node index"}), 400

    pitch = chord[node_index]
    fraction = Fraction(pitch.get("fraction", "1"))
    frequency = fundamental * float(fraction)
    voice = node_index + 1  # 1-indexed

    # Send to siren (192.168.4.200:54001) using current fundamental
    siren_sender = OSCSender(ip="192.168.4.200", port=54001, fundamental=fundamental)
    siren_sender.set_chords(chords)  # Set chords to ensure proper voice count
    siren_sender.send_single(frequency, voice)

    return jsonify(
        {
            "frequency": frequency,
            "voice": voice,
            "fundamental": fundamental,
            "fraction": pitch.get("fraction", "1"),
            "destination": "siren",
        }
    )


@app.route("/api/kill-siren", methods=["POST"])
def kill_siren():
    """Send kill message to siren (soft or hard)."""
    data = request.json
    soft = data.get("soft", True)  # default to soft kill

    kill_freq = 20.0 if soft else 0.0
    kill_type = "soft" if soft else "hard"

    # Send kill to all voices on siren using send_kill
    siren_sender = OSCSender(ip="192.168.4.200", port=54001, fundamental=fundamental)
    siren_sender.set_chords(chords)  # Set chords to get correct num_voices
    siren_sender.send_kill(soft=soft)

    return jsonify(
        {
            "kill_freq": kill_freq,
            "kill_type": kill_type,
            "voices": list(range(1, siren_sender.num_voices + 1)),
        }
    )


@app.route("/api/load-file", methods=["POST"])
def load_file():
    global current_index, chords
    data = request.json
    filepath = data.get("filepath")

    if not filepath:
        return jsonify({"error": "No filepath provided"}), 400

    try:
        with open(filepath) as f:
            chords_data = json.load(f)
            chords = chords_data.get("chords", [])
        current_index = 0
        return jsonify({"loaded": len(chords), "filepath": filepath})
    except FileNotFoundError:
        return jsonify({"error": f"File not found: {filepath}"}), 404
    except json.JSONDecodeError:
        return jsonify({"error": f"Invalid JSON in: {filepath}"}), 400


if __name__ == "__main__":
    print("Starting Path Navigator server...")
    print(f"Loading chords from: {get_chords_file()}")
    load_chords()
    print(f"Loaded {len(chords)} chords")
    app.run(host="0.0.0.0", port=8080, debug=True)
