#!/usr/bin/env python3
"""
Swirl Master — API Server v1.0
Backend voor de Ninja Swirl Receptencompanion

Biedt een REST API voor:
- Recepten ophalen, filteren en zoeken
- Recepten toevoegen (community)
- Mix & Match suggesties
- Programma-informatie
- Statistieken

Stack: Flask + SQLite
Port: 5060 (PM2: swirl-master)
"""

import os
import json
import sqlite3
import hashlib
from datetime import datetime, timezone
from functools import wraps
from flask import Flask, request, jsonify, send_from_directory, g

# ============================================================
# CONFIGURATION
# ============================================================

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DB_PATH = os.environ.get('SWIRL_DB_PATH', os.path.join(BASE_DIR, 'swirl_master.db'))
STATIC_DIR = BASE_DIR
PORT = int(os.environ.get('SWIRL_PORT', 5060))
DEBUG = os.environ.get('SWIRL_DEBUG', 'false').lower() == 'true'
API_KEY = os.environ.get('SWIRL_API_KEY', '')  # Optional API key for write endpoints

app = Flask(__name__, static_folder=None)

# ============================================================
# DATABASE
# ============================================================

def get_db():
    """Get a database connection for the current request."""
    if 'db' not in g:
        g.db = sqlite3.connect(DB_PATH)
        g.db.row_factory = sqlite3.Row
        g.db.execute('PRAGMA journal_mode=WAL')
        g.db.execute('PRAGMA foreign_keys=ON')
    return g.db

@app.teardown_appcontext
def close_db(exc):
    db = g.pop('db', None)
    if db is not None:
        db.close()

def init_db():
    """Initialize the database with schema and seed data."""
    conn = sqlite3.connect(DB_PATH)
    conn.executescript('''
        CREATE TABLE IF NOT EXISTS recipes (
            id TEXT PRIMARY KEY,
            type TEXT NOT NULL,
            name TEXT NOT NULL,
            emoji TEXT DEFAULT '🍦',
            description TEXT,
            difficulty TEXT,
            difficulty_score INTEGER DEFAULT 1,
            prep_time TEXT,
            freeze_time TEXT DEFAULT '24u',
            total_time TEXT,
            servings INTEGER DEFAULT 4,
            rating REAL DEFAULT 4.5,
            popularity INTEGER DEFAULT 50,
            tags TEXT,       -- JSON array
            diet TEXT,       -- JSON array
            program TEXT,
            ingredients TEXT, -- JSON array
            steps TEXT,      -- JSON array
            mixins TEXT,     -- JSON array
            nutrition TEXT,  -- JSON object
            tips TEXT,
            created_at TEXT DEFAULT (datetime('now')),
            updated_at TEXT DEFAULT (datetime('now')),
            is_custom INTEGER DEFAULT 0
        );

        CREATE TABLE IF NOT EXISTS favorites (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            session_id TEXT NOT NULL,
            recipe_id TEXT NOT NULL,
            created_at TEXT DEFAULT (datetime('now')),
            UNIQUE(session_id, recipe_id)
        );

        CREATE TABLE IF NOT EXISTS ratings (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            session_id TEXT NOT NULL,
            recipe_id TEXT NOT NULL,
            rating INTEGER NOT NULL CHECK(rating >= 1 AND rating <= 5),
            created_at TEXT DEFAULT (datetime('now')),
            UNIQUE(session_id, recipe_id)
        );

        CREATE INDEX IF NOT EXISTS idx_recipes_type ON recipes(type);
        CREATE INDEX IF NOT EXISTS idx_recipes_popularity ON recipes(popularity DESC);
        CREATE INDEX IF NOT EXISTS idx_favorites_session ON favorites(session_id);
    ''')
    conn.commit()
    seed_recipes(conn)
    conn.close()

def seed_recipes(conn):
    """Seed the database with built-in recipes if empty."""
    count = conn.execute('SELECT COUNT(*) FROM recipes WHERE is_custom = 0').fetchone()[0]
    if count > 0:
        return

    seed_file = os.path.join(BASE_DIR, 'seed_recipes.json')
    if not os.path.exists(seed_file):
        return

    with open(seed_file, 'r', encoding='utf-8') as f:
        recipes = json.load(f)

    for r in recipes:
        conn.execute('''
            INSERT OR IGNORE INTO recipes
            (id, type, name, emoji, description, difficulty, difficulty_score,
             prep_time, freeze_time, total_time, servings, rating, popularity,
             tags, diet, program, ingredients, steps, mixins, nutrition, tips, is_custom)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
        ''', (
            r['id'], r['type'], r['name'], r.get('emoji', '🍦'),
            r.get('description', ''), r.get('difficulty', 'Beginner'),
            r.get('difficultyScore', 1), r.get('prepTime', '5 min'),
            r.get('freezeTime', '24u'), r.get('totalTime', '24u 5m'),
            r.get('servings', 4), r.get('rating', 4.5),
            r.get('popularity', 50),
            json.dumps(r.get('tags', [])),
            json.dumps(r.get('diet', [])),
            r.get('program', 'Ice Cream'),
            json.dumps(r.get('ingredients', [])),
            json.dumps(r.get('steps', [])),
            json.dumps(r.get('mixins', [])),
            json.dumps(r.get('nutrition', {})),
            r.get('tips', '')
        ))
    conn.commit()

# ============================================================
# AUTH (optional API key for write operations)
# ============================================================

def require_api_key(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        if API_KEY:
            provided = request.headers.get('X-API-Key', '')
            if provided != API_KEY:
                return jsonify({'error': 'Unauthorized', 'message': 'Invalid or missing API key'}), 401
        return f(*args, **kwargs)
    return decorated

# ============================================================
# HELPERS
# ============================================================

def row_to_recipe(row):
    """Convert a database row to a recipe dict."""
    return {
        'id': row['id'],
        'type': row['type'],
        'name': row['name'],
        'emoji': row['emoji'],
        'description': row['description'],
        'difficulty': row['difficulty'],
        'difficultyScore': row['difficulty_score'],
        'prepTime': row['prep_time'],
        'freezeTime': row['freeze_time'],
        'totalTime': row['total_time'],
        'servings': row['servings'],
        'rating': row['rating'],
        'popularity': row['popularity'],
        'tags': json.loads(row['tags']) if row['tags'] else [],
        'diet': json.loads(row['diet']) if row['diet'] else [],
        'program': row['program'],
        'ingredients': json.loads(row['ingredients']) if row['ingredients'] else [],
        'steps': json.loads(row['steps']) if row['steps'] else [],
        'mixins': json.loads(row['mixins']) if row['mixins'] else [],
        'nutrition': json.loads(row['nutrition']) if row['nutrition'] else {},
        'tips': row['tips'],
        'createdAt': row['created_at'],
        'updatedAt': row['updated_at'],
        'isCustom': bool(row['is_custom']),
    }

def get_session_id():
    """Extract or generate a session ID."""
    sid = request.headers.get('X-Session-ID', '')
    if not sid:
        sid = 'anon-' + hashlib.md5(request.remote_addr.encode()).hexdigest()[:12]
    return sid

# ============================================================
# STATIC ROUTES (serve the webapp)
# ============================================================

@app.route('/')
def index():
    resp = send_from_directory(STATIC_DIR, 'index.html')
    resp.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
    resp.headers['CDN-Cache-Control'] = 'no-store'
    resp.headers['Pragma'] = 'no-cache'
    resp.headers['Expires'] = '0'
    return resp

@app.route('/<path:filename>')
def static_files(filename):
    if '..' in filename or filename.startswith('/'):
        return jsonify({'error': 'Forbidden'}), 403
    full_path = os.path.join(STATIC_DIR, filename)
    if os.path.isfile(full_path):
        resp = send_from_directory(STATIC_DIR, filename)
        # No caching for JS/CSS to ensure latest version always loads
        if filename.endswith('.js') or filename.endswith('.css'):
            resp.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
            resp.headers['CDN-Cache-Control'] = 'no-store'
            resp.headers['Pragma'] = 'no-cache'
            resp.headers['Expires'] = '0'
        return resp
    return jsonify({'error': 'Not found'}), 404

# ============================================================
# API: RECIPES
# ============================================================

@app.route('/api/recipes', methods=['GET'])
def api_get_recipes():
    """
    GET /api/recipes?type=ice_cream&diet=vegan&sort=popular&q=chocolate&limit=20

    Returns a list of recipes matching the filters.
    """
    db = get_db()

    query = 'SELECT * FROM recipes WHERE 1=1'
    params = []

    # Type filter
    recipe_type = request.args.get('type')
    if recipe_type and recipe_type != 'all':
        query += ' AND type = ?'
        params.append(recipe_type)

    # Diet filters (JSON array search)
    diet = request.args.getlist('diet')
    for d in diet:
        query += ' AND diet LIKE ?'
        params.append(f'%"{d}"%')

    # Search
    q = request.args.get('q', '').strip().lower()
    if q:
        query += ' AND (LOWER(name) LIKE ? OR LOWER(description) LIKE ? OR LOWER(ingredients) LIKE ? OR LOWER(tags) LIKE ?)'
        params.extend([f'%{q}%' for _ in range(4)])

    # Sort
    sort = request.args.get('sort', 'popular')
    sort_map = {
        'popular': 'popularity DESC',
        'easy': 'difficulty_score ASC',
        'rating': 'rating DESC',
        'name': 'name ASC',
    }
    query += f' ORDER BY {sort_map.get(sort, "popularity DESC")}'

    # Limit
    limit = min(int(request.args.get('limit', 100)), 200)
    query += ' LIMIT ?'
    params.append(limit)

    rows = db.execute(query, params).fetchall()
    return jsonify({
        'count': len(rows),
        'recipes': [row_to_recipe(r) for r in rows]
    })

@app.route('/api/recipes/<recipe_id>', methods=['GET'])
def api_get_recipe(recipe_id):
    """GET /api/recipes/<id> — Get a single recipe by ID."""
    db = get_db()
    row = db.execute('SELECT * FROM recipes WHERE id = ?', (recipe_id,)).fetchone()
    if not row:
        return jsonify({'error': 'Not found', 'message': f'Recipe {recipe_id} not found'}), 404
    return jsonify(row_to_recipe(row))

@app.route('/api/recipes', methods=['POST'])
@require_api_key
def api_add_recipe():
    """POST /api/recipes — Add a custom recipe. Requires API key if configured."""
    data = request.get_json()
    if not data or not data.get('name') or not data.get('type'):
        return jsonify({'error': 'Bad request', 'message': 'name and type are required'}), 400

    db = get_db()
    recipe_id = data.get('id') or f"custom-{int(datetime.now().timestamp())}"

    try:
        db.execute('''
            INSERT OR REPLACE INTO recipes
            (id, type, name, emoji, description, difficulty, difficulty_score,
             prep_time, freeze_time, total_time, servings, rating, popularity,
             tags, diet, program, ingredients, steps, mixins, nutrition, tips, is_custom)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
        ''', (
            recipe_id, data['type'], data['name'], data.get('emoji', '🍦'),
            data.get('description', ''), data.get('difficulty', 'Custom'),
            data.get('difficultyScore', 2),
            data.get('prepTime', '5 min'), data.get('freezeTime', '24u'),
            data.get('totalTime', '24u 5m'),
            data.get('servings', 4), data.get('rating', 4.0),
            data.get('popularity', 0),
            json.dumps(data.get('tags', ['custom'])),
            json.dumps(data.get('diet', [])),
            data.get('program', 'Ice Cream'),
            json.dumps(data.get('ingredients', [])),
            json.dumps(data.get('steps', [])),
            json.dumps(data.get('mixins', [])),
            json.dumps(data.get('nutrition', {})),
            data.get('tips', '')
        ))
        db.commit()
        return jsonify({'status': 'created', 'id': recipe_id}), 201
    except Exception as e:
        return jsonify({'error': 'Database error', 'message': str(e)}), 500

# ============================================================
# API: PROGRAMS
# ============================================================

PROGRAMS_DATA = {
    "ice_cream":      {"name": "Ice Cream",        "category": "scooped", "icon": "🍨", "desc": "Romige, scooped iJs — de klassieker."},
    "lite_ice_cream": {"name": "Lite Ice Cream",   "category": "scooped", "icon": "🥛", "desc": "Lichtere iJs met minder vet en suiker."},
    "gelato":         {"name": "Gelato",           "category": "scooped", "icon": "🇮🇹", "desc": "Italiaanse dichte, zijdezachte iJs."},
    "sorbet":         {"name": "Sorbet",           "category": "scooped", "icon": "🥭", "desc": "Fruitig, zuivelvrij en verfrissend."},
    "milkshake":      {"name": "Milkshake",        "category": "scooped", "icon": "🥤", "desc": "Dikke, drinkbare traktatie."},
    "smoothie_bowl":  {"name": "Smoothie Bowl",    "category": "scooped", "icon": "🥣", "desc": "Gezonde, lepelbare fruitschotel."},
    "frozen_yogurt":  {"name": "Frozen Yogurt",    "category": "scooped", "icon": "🥛", "desc": "Frisse, romige bevroren yoghurt."},
    "soft_serve":     {"name": "Soft Serve",       "category": "soft",    "icon": "🍦", "desc": "Zachte, draaiende iJs — fair stijl."},
    "froyo_soft":     {"name": "Frozen Yogurt Soft","category": "soft",   "icon": "🥛", "desc": "Zachte serve frozen yogurt."},
    "custard":        {"name": "Custard",          "category": "soft",    "icon": "🥚", "desc": "Rijke, eidooier-gebaseerde soft serve."},
    "fruit_whip":     {"name": "Fruit Whip",       "category": "soft",    "icon": "🍓", "desc": "Luchtige, fruitige soft serve."},
    "high_protein":   {"name": "High Protein Soft","category": "soft",    "icon": "💪", "desc": "Eiwitrijke soft serve voor na de workout."},
    "lite_soft":      {"name": "Lite Soft Serve",  "category": "soft",    "icon": "🌿", "desc": "Lichtere soft serve variant."},
}

@app.route('/api/programs', methods=['GET'])
def api_get_programs():
    """GET /api/programs — List all 13 programs."""
    return jsonify({"count": len(PROGRAMS_DATA), "programs": PROGRAMS_DATA})

@app.route('/api/programs/<program_id>', methods=['GET'])
def api_get_program(program_id):
    """GET /api/programs/<id> — Get a single program."""
    if program_id not in PROGRAMS_DATA:
        return jsonify({'error': 'Not found'}), 404
    return jsonify(PROGRAMS_DATA[program_id])

# ============================================================
# API: FAVORITES
# ============================================================

@app.route('/api/favorites', methods=['GET'])
def api_get_favorites():
    """GET /api/favorites — Get user's favorites (by session ID)."""
    sid = get_session_id()
    db = get_db()
    rows = db.execute('''
        SELECT r.* FROM favorites f
        JOIN recipes r ON f.recipe_id = r.id
        WHERE f.session_id = ?
        ORDER BY f.created_at DESC
    ''', (sid,)).fetchall()
    return jsonify({
        'count': len(rows),
        'sessionId': sid,
        'favorites': [row_to_recipe(r) for r in rows]
    })

@app.route('/api/favorites/<recipe_id>', methods=['POST'])
def api_add_favorite(recipe_id):
    """POST /api/favorites/<recipe_id> — Add a recipe to favorites."""
    sid = get_session_id()
    db = get_db()

    # Check recipe exists
    recipe = db.execute('SELECT id FROM recipes WHERE id = ?', (recipe_id,)).fetchone()
    if not recipe:
        return jsonify({'error': 'Not found', 'message': f'Recipe {recipe_id} not found'}), 404

    try:
        db.execute('INSERT OR IGNORE INTO favorites (session_id, recipe_id) VALUES (?, ?)', (sid, recipe_id))
        db.commit()
    except Exception as e:
        return jsonify({'error': 'Database error', 'message': str(e)}), 500

    return jsonify({'status': 'added', 'recipeId': recipe_id, 'sessionId': sid})

@app.route('/api/favorites/<recipe_id>', methods=['DELETE'])
def api_remove_favorite(recipe_id):
    """DELETE /api/favorites/<recipe_id> — Remove a recipe from favorites."""
    sid = get_session_id()
    db = get_db()
    db.execute('DELETE FROM favorites WHERE session_id = ? AND recipe_id = ?', (sid, recipe_id))
    db.commit()
    return jsonify({'status': 'removed', 'recipeId': recipe_id})

# ============================================================
# API: RATINGS
# ============================================================

@app.route('/api/ratings/<recipe_id>', methods=['POST'])
def api_rate_recipe(recipe_id):
    """POST /api/ratings/<recipe_id> — Rate a recipe (1-5 stars)."""
    sid = get_session_id()
    data = request.get_json() or {}
    rating = data.get('rating')

    if not rating or not isinstance(rating, int) or rating < 1 or rating > 5:
        return jsonify({'error': 'Bad request', 'message': 'rating must be an integer 1-5'}), 400

    db = get_db()
    recipe = db.execute('SELECT id FROM recipes WHERE id = ?', (recipe_id,)).fetchone()
    if not recipe:
        return jsonify({'error': 'Not found'}), 404

    db.execute('''
        INSERT OR REPLACE INTO ratings (session_id, recipe_id, rating)
        VALUES (?, ?, ?)
    ''', (sid, recipe_id, rating))
    db.commit()

    # Update recipe average rating
    avg = db.execute('SELECT AVG(rating) as avg, COUNT(*) as cnt FROM ratings WHERE recipe_id = ?', (recipe_id,)).fetchone()
    if avg and avg['cnt'] > 0:
        new_rating = round(avg['avg'], 1)
        db.execute('UPDATE recipes SET rating = ?, updated_at = datetime("now") WHERE id = ?', (new_rating, recipe_id))
        db.commit()

    return jsonify({'status': 'rated', 'recipeId': recipe_id, 'rating': rating, 'average': new_rating if avg and avg['cnt'] > 0 else None})

# ============================================================
# API: MIX & MATCH
# ============================================================

MIXMATCH_PAIRING = {
    "chocolate": {"choc-chunks": 1.3, "brownie": 1.4, "caramel-swirl": 1.2, "espresso-beans": 1.3},
    "vanilla": {"choc-chunks": 1.2, "oreo": 1.3, "cookie-dough": 1.3, "fresh-berries": 1.2},
    "strawberry": {"fresh-berries": 1.3, "granola": 1.2, "marshmallow": 1.2},
    "mango": {"coconut-flakes": 1.3, "chili": 1.4},
    "coffee": {"espresso-beans": 1.4, "choc-chunks": 1.3, "caramel-swirl": 1.3},
    "caramel": {"caramel-swirl": 1.5, "pecan": 1.3, "sea-salt": 1.4},
}

@app.route('/api/mixmatch/suggest', methods=['POST'])
def api_mixmatch_suggest():
    """
    POST /api/mixmatch/suggest
    Body: {"base": "heavy-cream", "flavor": "chocolate", "mixin": "brownie"}

    Returns a generated recipe with compatibility score and program recommendation.
    """
    data = request.get_json() or {}
    base = data.get('base')
    flavor = data.get('flavor')
    mixin = data.get('mixin')
    topping = data.get('topping')

    # Calculate compatibility
    score = 60
    if flavor and mixin and flavor in MIXMATCH_PAIRING and mixin in MIXMATCH_PAIRING[flavor]:
        score = int(score * MIXMATCH_PAIRING[flavor][mixin])
    elif flavor:
        score = min(90, score + 15)

    # Determine program
    program = "Ice Cream"
    if base == "greek-yogurt":
        program = "Frozen Yogurt"
    elif base == "protein-shake":
        program = "High Protein Soft"
    elif base in ("coconut-milk", "almond-milk") and flavor in ("mango", "lemon", "strawberry"):
        program = "Sorbet"

    # Build ingredient list
    base_names = {
        "heavy-cream": "zware room",
        "whole-milk": "volle melk",
        "coconut-milk": "kokosmelk",
        "almond-milk": "amandelmelk",
        "greek-yogurt": "Griekse yoghurt",
        "oat-milk": "havermelk",
        "cashew-milk": "cashewmelk",
        "protein-shake": "eiwitschuddishek",
    }

    ingredients = [f"1 kopje {base_names.get(base, 'zware room')} (237ml)"]
    ingredients.append("½ kopje melk naar keuze (120ml)")
    ingredients.append("⅓ kopje suiker (67g)")

    flavor_ingredients = {
        "vanilla": "½ tl vanille-extract",
        "chocolate": "1½ el cacaopoeder",
        "strawberry": "½ kopje aardbeien puree",
        "mango": "½ kopje mango puree",
        "coffee": "1 el oploskoffie",
        "matcha": "1 el matcha poeder",
        "caramel": "1½ el karamelsiroop",
        "mint": "½ tl pepermunt extract",
    }
    if flavor and flavor in flavor_ingredients:
        ingredients.append(flavor_ingredients[flavor])
    if mixin:
        ingredients.append(f"Mix-in: {mixin}")
    ingredients.append("Snufje zout")

    label = "Perfecte match!" if score >= 85 else "Goede combi!" if score >= 70 else "Avant-garde..."

    return jsonify({
        "compatibility": score,
        "label": label,
        "program": program,
        "ingredients": ingredients,
        "steps": [
            "Mix alle ingrediënten in een kom tot een gladde massa.",
            "Giet in een Ninja Swirl pint tot 2,5 cm van de rand.",
            "Plaats de deksel en vries minimaal 24 uur.",
            f"Verwerk op het '{program}' programma.",
            "RE-SPIN met 1 el melk indien het iJs te vast is.",
        ],
        "selections": {"base": base, "flavor": flavor, "mixin": mixin, "topping": topping},
    })

# ============================================================
# API: STATS
# ============================================================

@app.route('/api/stats', methods=['GET'])
def api_get_stats():
    """GET /api/stats — Get overall statistics."""
    db = get_db()
    total_recipes = db.execute('SELECT COUNT(*) FROM recipes').fetchone()[0]
    total_custom = db.execute('SELECT COUNT(*) FROM recipes WHERE is_custom = 1').fetchone()[0]
    by_type = {}
    for row in db.execute('SELECT type, COUNT(*) as cnt FROM recipes GROUP BY type').fetchall():
        by_type[row['type']] = row['cnt']

    return jsonify({
        "totalRecipes": total_recipes,
        "totalCustom": total_custom,
        "totalPrograms": 13,
        "recipesByType": by_type,
        "database": "swirl_master.db",
        "version": "1.0",
        "timestamp": datetime.now(timezone.utc).isoformat()
    })

# ============================================================
# API: HEALTH CHECK
# ============================================================

@app.route('/api/health', methods=['GET'])
def api_health():
    """GET /api/health — Health check endpoint."""
    db = get_db()
    try:
        db.execute('SELECT 1').fetchone()
        db_ok = True
    except Exception:
        db_ok = False

    return jsonify({
        "status": "healthy" if db_ok else "degraded",
        "database": db_ok,
        "version": "1.0",
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "port": PORT,
    })

# ============================================================
# MAIN
# ============================================================

if __name__ == '__main__':
    init_db()
    print(f"🍦 Swirl Master API Server v1.0")
    print(f"   Database: {DB_PATH}")
    print(f"   Static: {STATIC_DIR}")
    print(f"   Port: {PORT}")
    print(f"   Debug: {DEBUG}")
    print(f"   Starting...")
    app.run(host='0.0.0.0', port=PORT, debug=DEBUG)
else:
    # For WSGI servers (gunicorn)
    init_db()
