#!/usr/bin/env python3
"""
Swirl Master — Scale All Recipes to 1 Ninja Swirl Pint (16oz / 473ml)

The Ninja Swirl by CREAMi uses 16oz (473ml) pints.
You fill to the max line (~420ml usable).
All recipes are scaled so the total volume fits exactly 1 swirl pint.

Strategy:
- servings=4 recipes: scale factor 0.5 (they're typically ~710ml, halved = ~355ml ✓)
- servings=2 recipes: scale factor 1.0 (already ~1 pint, keep as-is)
- servings=1 recipes: no change

Updates: app.js (RECIPES array), seed_recipes.json, swirl_master.db
"""

import json
import re
import sqlite3
import os
import subprocess

BASE_DIR = os.path.dirname(os.path.abspath(__file__))

# ============================================================
# FRACTION HELPERS
# ============================================================

FRACTION_MAP = {
    0.125: '⅛', 0.25: '¼', 0.333: '⅓', 0.375: '⅜',
    0.5: '½', 0.625: '⅝', 0.667: '⅔', 0.75: '¾', 0.875: '⅞',
}

def to_fraction(n):
    """Convert a number to a nice fraction string."""
    if n == 0:
        return '0'
    whole = int(n)
    frac = n - whole
    # Round to nearest common fraction
    for fval, fstr in FRACTION_MAP.items():
        if abs(frac - fval) < 0.04:
            if whole == 0:
                return fstr
            return f"{whole}{fstr}"
    # Round to nearest 0.25
    rounded = round(n * 4) / 4
    if rounded == int(rounded):
        return str(int(rounded))
    whole = int(rounded)
    frac = rounded - whole
    for fval, fstr in FRACTION_MAP.items():
        if abs(frac - fval) < 0.04:
            if whole == 0:
                return fstr
            return f"{whole}{fstr}"
    # Fallback: decimal
    return f"{n:.1f}".rstrip('0').rstrip('.')


def scale_ingredient(ing, factor):
    """Scale a single ingredient string by factor."""
    if factor == 1.0:
        return ing

    # Don't scale "Snufje", "Een snufje", "Optioneel", etc.
    lower = ing.lower().strip()
    if lower.startswith('snufje') or lower.startswith('een snufje'):
        return ing
    if lower.startswith('pinch') or lower.startswith('dash'):
        return ing

    result = ing

    # Pattern 1: Mixed fractions at start like "1½", "2⅔", etc.
    # Match: number + optional fraction char + unit
    frac_chars = '½⅓⅔¼¾⅛⅜⅝⅞'
    
    def replace_mixed_frac(m):
        whole = m.group(1)
        frac_char = m.group(2) or ''
        unit = m.group(3)
        
        # Parse value
        val = float(whole) if whole else 0
        char_to_val = {'½':0.5,'⅓':0.333,'⅔':0.667,'¼':0.25,'¾':0.75,'⅛':0.125,'⅜':0.375,'⅝':0.625,'⅞':0.875}
        if frac_char in char_to_val:
            val += char_to_val[frac_char]
        
        new_val = val * factor
        if new_val < 0.1:
            return m.group(0)  # too small, keep original
        
        # Determine singular/plural
        if abs(new_val - 1.0) < 0.04 and unit.endswith('s'):
            unit = unit[:-1]  # make singular
        elif abs(new_val - 1.0) > 0.04 and not unit.endswith('s') and unit in ('kopje',):
            unit = unit + 's'  # make plural
        
        return to_fraction(new_val) + ' ' + unit

    # SINGLE COMBINED PATTERN: matches both "1½ kopje" and "½ kopje" in one pass
    # This prevents double-scaling from two separate regex passes
    char_to_val = {'½':0.5,'⅓':0.333,'⅔':0.667,'¼':0.25,'¾':0.75,'⅛':0.125,'⅜':0.375,'⅝':0.625,'⅞':0.875}
    units_pattern = 'kopjes|kopje|cups|cup|eetlepels|eetlepel|theelepels|theelepel'

    def replace_any_qty(m):
        whole = m.group(1)  # digit or None
        frac1 = m.group(2)  # fraction after digit or None
        frac2 = m.group(3)  # standalone fraction or None
        unit = m.group(4)

        val = 0.0
        if whole:
            val += float(whole)
        if frac1 and frac1 in char_to_val:
            val += char_to_val[frac1]
        if frac2 and frac2 in char_to_val:
            val += char_to_val[frac2]

        if val == 0:
            return m.group(0)

        new_val = val * factor
        if new_val < 0.1:
            return m.group(0)  # too small

        # Singular/plural: Dutch uses singular for < 1 and = 1, plural for > 1
        if new_val <= 1.0:
            if unit.endswith('s'):
                unit = unit[:-1]  # singular: kopje, eetlepel, theelepel
        else:
            if unit in ('kopje', 'eetlepel', 'theelepel'):
                unit = unit + 's'  # plural: kopjes, eetlepels, theelepels

        return to_fraction(new_val) + ' ' + unit

    # Matches: "1½ kopje", "1 kopje", "½ kopje", "⅔ kopje"
    combined = r'(?:(\d+)([' + frac_chars + '])?|([' + frac_chars + ']))\s+(' + units_pattern + ')'
    result = re.sub(combined, replace_any_qty, result)

    # Pattern: number + ' blik' (can)
    def replace_blik(m):
        val = float(m.group(1))
        new_val = val * factor
        unit = 'blik' if abs(new_val - 1.0) < 0.04 else 'blik'
        return to_fraction(new_val) + ' ' + unit
    result = re.sub(r'(\d+)\s+(blik)\b', replace_blik, result)

    # Pattern: number + ' packets'
    def replace_packets(m):
        val = float(m.group(1))
        new_val = val * factor
        unit_word = m.group(2)
        return to_fraction(new_val) + ' ' + unit_word
    result = re.sub(r'(\d+)\s+(packets|blikjes|stukjes|ten)\b', replace_packets, result)

    # Pattern: bare numbers (eidooiers, eieren, etc.) — only whole numbers > 0 at start
    def replace_count(m):
        val = float(m.group(1))
        unit_word = m.group(2)
        new_val = val * factor
        if new_val >= 1:
            return str(int(round(new_val))) + ' ' + unit_word
        return m.group(0)  # keep if < 1
    result = re.sub(r'^(\d+)\s+(eidooiers|eieren|eieren,|bananen|bananen,)', replace_count, result, flags=re.IGNORECASE)

    # Pattern: (Xg) — grams in parentheses
    def replace_grams(m):
        val = float(m.group(1))
        new_val = round(val * factor)
        return f'({new_val}g)'
    result = re.sub(r'\((\d+)g\)', replace_grams, result)

    # Pattern: (Xml) — milliliters in parentheses
    def replace_ml(m):
        val = float(m.group(1))
        new_val = round(val * factor)
        return f'({new_val}ml)'
    result = re.sub(r'\((\d+)ml\)', replace_ml, result)

    # Pattern: Xg at start (not in parens) — e.g. "300g diepgevroren"
    def replace_leading_grams(m):
        val = float(m.group(1))
        new_val = round(val * factor)
        rest = m.group(2)
        return f'{new_val}g {rest}'
    result = re.sub(r'^(\d+)g\s+(.+)', replace_leading_grams, result)

    # Pattern: Xml at start (not in parens) — e.g. "100ml amandelmelk"
    def replace_leading_ml(m):
        val = float(m.group(1))
        new_val = round(val * factor)
        rest = m.group(2)
        return f'{new_val}ml {rest}'
    result = re.sub(r'^(\d+)ml\s+(.+)', replace_leading_ml, result)

    return result


def scale_nutrition(nutri, factor):
    """Scale nutrition values."""
    if not nutri or factor == 1.0:
        return nutri
    result = {}
    for k, v in nutri.items():
        if isinstance(v, (int, float)):
            result[k] = round(v * factor)
        elif isinstance(v, str):
            # Parse "22g" format
            m = re.match(r'(\d+)(g?)', v)
            if m:
                result[k] = f"{round(int(m.group(1)) * factor)}{m.group(2)}"
            else:
                result[k] = v
        else:
            result[k] = v
    return result


# ============================================================
# MAIN SCALING LOGIC
# ============================================================

def scale_recipes(recipes):
    """Scale all recipes to 1 pint portions."""
    scaled = []
    for r in recipes:
        recipe = dict(r)
        servings = r.get('servings', 4)

        if servings >= 3:
            factor = 420.0 / 710.0  # ~0.59 → round to 0.5 for clean numbers
            factor = 0.5
        elif servings == 2:
            factor = 1.0  # Already roughly 1 pint
        else:
            factor = 1.0

        if factor != 1.0:
            recipe['ingredients'] = [scale_ingredient(ing, factor) for ing in r['ingredients']]
            recipe['nutrition'] = scale_nutrition(r.get('nutrition', {}), factor)
            # Scale calories in description if present
            if isinstance(recipe.get('nutrition'), dict) and 'calories' in recipe['nutrition']:
                recipe['nutrition']['calories'] = round(recipe['nutrition']['calories'])

        recipe['servings'] = 1
        recipe['pintInfo'] = '1 swirl pint (±3 porties)'

        # Update steps that mention amounts
        steps = recipe.get('steps', [])
        new_steps = []
        for step in steps:
            new_step = step
            # Fix "2,5 cm ruimte" stays same
            # Fix references to old amounts
            if factor == 0.5:
                new_step = new_step.replace('2 kopjes', '1 kopje').replace('1 kopje', '½ kopje')
            new_steps.append(new_step)
        recipe['steps'] = new_steps

        scaled.append(recipe)

    return scaled


def update_appjs(new_recipes):
    """Update the RECIPES array in app.js."""
    with open(os.path.join(BASE_DIR, 'app.js'), 'r', encoding='utf-8') as f:
        content = f.read()

    start_marker = 'const RECIPES = ['
    start = content.index(start_marker)
    # Find the closing '];' 
    bracket_depth = 0
    pos = start + len(start_marker) - 1  # at the '['
    while pos < len(content):
        if content[pos] == '[':
            bracket_depth += 1
        elif content[pos] == ']':
            bracket_depth -= 1
            if bracket_depth == 0:
                break
        pos += 1
    
    end = pos + 1  # after ']'
    # Include the semicolon if present
    if end < len(content) and content[end] == ';':
        end += 1

    # Format recipes as JS
    js_lines = ['const RECIPES = [']
    for i, r in enumerate(new_recipes):
        js_obj = format_recipe_as_js(r, indent='  ')
        comma = ',' if i < len(new_recipes) - 1 else ''
        js_lines.append(js_obj + comma)
    js_lines.append('];')

    new_content = content[:start] + '\n'.join(js_lines) + content[end:]
    
    with open(os.path.join(BASE_DIR, 'app.js'), 'w', encoding='utf-8') as f:
        f.write(new_content)
    
    print(f"✅ app.js updated with {len(new_recipes)} scaled recipes")


def format_recipe_as_js(r, indent=''):
    """Format a recipe dict as a JS object literal."""
    lines = [f'{indent}{{']
    lines.append(f'{indent}  id: {json.dumps(r["id"])},')
    lines.append(f'{indent}  type: {json.dumps(r["type"])},')
    lines.append(f'{indent}  name: {json.dumps(r["name"])},')
    lines.append(f'{indent}  emoji: {json.dumps(r.get("emoji", "🍦"))},')
    lines.append(f'{indent}  description: {json.dumps(r["description"])},')
    lines.append(f'{indent}  difficulty: {json.dumps(r["difficulty"])},')
    lines.append(f'{indent}  difficultyScore: {r.get("difficultyScore", 1)},')
    lines.append(f'{indent}  prepTime: {json.dumps(r.get("prepTime", "5 min"))},')
    lines.append(f'{indent}  freezeTime: {json.dumps(r.get("freezeTime", "24u"))},')
    lines.append(f'{indent}  totalTime: {json.dumps(r.get("totalTime", "24u 5m"))},')
    lines.append(f'{indent}  servings: 1,')
    if r.get('pintInfo'):
        lines.append(f'{indent}  pintInfo: {json.dumps(r["pintInfo"])},')
    lines.append(f'{indent}  rating: {r.get("rating", 4.5)},')
    lines.append(f'{indent}  popularity: {r.get("popularity", 50)},')
    
    # tags
    tags = r.get('tags', [])
    if tags:
        tag_str = ', '.join(json.dumps(t) for t in tags)
        lines.append(f'{indent}  tags: [{tag_str}],')
    else:
        lines.append(f'{indent}  tags: [],')
    
    # diet
    diet = r.get('diet', [])
    if diet:
        diet_str = ', '.join(json.dumps(d) for d in diet)
        lines.append(f'{indent}  diet: [{diet_str}],')
    else:
        lines.append(f'{indent}  diet: [],')
    
    lines.append(f'{indent}  program: {json.dumps(r.get("program", "Ice Cream"))},')
    
    # ingredients
    ings = r.get('ingredients', [])
    if len(ings) == 1:
        lines.append(f'{indent}  ingredients: [{json.dumps(ings[0])}],')
    else:
        lines.append(f'{indent}  ingredients: [')
        for j, ing in enumerate(ings):
            comma = ',' if j < len(ings) - 1 else ''
            lines.append(f'{indent}    {json.dumps(ing)}{comma}')
        lines.append(f'{indent}  ],')
    
    # steps
    steps = r.get('steps', [])
    if len(steps) == 1:
        lines.append(f'{indent}  steps: [{json.dumps(steps[0])}],')
    else:
        lines.append(f'{indent}  steps: [')
        for j, step in enumerate(steps):
            comma = ',' if j < len(steps) - 1 else ''
            lines.append(f'{indent}    {json.dumps(step)}{comma}')
        lines.append(f'{indent}  ],')
    
    # mixins
    mixins = r.get('mixins', [])
    if mixins:
        mixin_str = ', '.join(json.dumps(m) for m in mixins)
        lines.append(f'{indent}  mixins: [{mixin_str}],')
    else:
        lines.append(f'{indent}  mixins: [],')
    
    # nutrition
    nutri = r.get('nutrition', {})
    if nutri:
        nutri_parts = []
        for k, v in nutri.items():
            nutri_parts.append(f'{k}: {v if isinstance(v, (int, float)) else json.dumps(v)}')
        lines.append(f'{indent}  nutrition: {{ {", ".join(nutri_parts)} }},')
    else:
        lines.append(f'{indent}  nutrition: {{}},')
    
    lines.append(f'{indent}  tips: {json.dumps(r.get("tips", ""))}')
    lines.append(f'{indent}}}')
    
    return '\n'.join(lines)


def update_database(recipes):
    """Update the SQLite database with scaled recipes."""
    db_path = os.path.join(BASE_DIR, 'swirl_master.db')
    conn = sqlite3.connect(db_path)
    
    for r in recipes:
        conn.execute('''
            UPDATE recipes SET
                ingredients = ?,
                nutrition = ?,
                servings = 1,
                updated_at = datetime('now')
            WHERE id = ?
        ''', (
            json.dumps(r['ingredients'], ensure_ascii=False),
            json.dumps(r['nutrition'], ensure_ascii=False),
            r['id']
        ))
    
    conn.commit()
    
    # Verify
    count = conn.execute('SELECT COUNT(*) FROM recipes').fetchone()[0]
    print(f"✅ Database updated: {count} recipes scaled to 1 pint")
    conn.close()


def update_seed_json(recipes):
    """Update seed_recipes.json with scaled versions of the original 5."""
    # The original 5 seed recipe IDs
    seed_ids = {'ic-vanilla', 'ic-chocolate', 'so-mango', 'ss-vanilla', 'fy-classic-tart'}
    seed_recipes = [r for r in recipes if r['id'] in seed_ids]
    
    with open(os.path.join(BASE_DIR, 'seed_recipes.json'), 'w', encoding='utf-8') as f:
        json.dump(seed_recipes, f, indent=2, ensure_ascii=False)
    
    print(f"✅ seed_recipes.json updated with {len(seed_recipes)} scaled recipes")


# ============================================================
# RUN
# ============================================================

if __name__ == '__main__':
    print("🍦 Swirl Master — Scaling all recipes to 1 pint (16oz / 473ml)")
    print("=" * 60)

    # Export recipes from app.js using node
    export_script = """
const fs = require('fs');
const code = fs.readFileSync('app.js', 'utf8');
const start = code.indexOf('const RECIPES = [');
let depth = 0, pos = start + 'const RECIPES = '.length - 1;
while (pos < code.length) {
    if (code[pos] === '[') depth++;
    else if (code[pos] === ']') { depth--; if (depth === 0) break; }
    pos++;
}
const arrayCode = code.substring(start + 'const RECIPES = '.length, pos + 1);
const recipes = eval(arrayCode);
fs.writeFileSync('/tmp/recipes_input.json', JSON.stringify(recipes, null, 2));
console.log('Exported ' + recipes.length + ' recipes from app.js');
"""
    with open('/tmp/export_recipes.js', 'w') as f:
        f.write(export_script)
    
    subprocess.run(['node', '/tmp/export_recipes.js'], cwd=BASE_DIR, check=True)

    with open('/tmp/recipes_input.json', 'r') as f:
        recipes = json.load(f)

    # Scale
    scaled = scale_recipes(recipes)

    # Count changes
    changed = sum(1 for r in scaled if any(r.get('pintInfo')))
    print(f"\n📊 Scaling complete:")
    print(f"   Total recipes: {len(scaled)}")
    print(f"   Scaled (servings≥3): {changed}")
    print(f"   Already 1-pint (servings≤2): {len(scaled) - changed}")

    # Show sample before/after
    print("\n📋 Sample — Vanille IJs (before → after):")
    for orig, scaled_r in zip(recipes, scaled):
        if orig['id'] == 'ic-vanilla':
            print("   BEFORE:")
            for ing in orig['ingredients']:
                print(f"     • {ing}")
            print("   AFTER:")
            for ing in scaled_r['ingredients']:
                print(f"     • {ing}")
            break

    print("\n📋 Sample — Mango Sorbet (before → after):")
    for orig, scaled_r in zip(recipes, scaled):
        if orig['id'] == 'so-mango':
            print("   BEFORE:")
            for ing in orig['ingredients']:
                print(f"     • {ing}")
            print("   AFTER:")
            for ing in scaled_r['ingredients']:
                print(f"     • {ing}")
            break

    # Backup originals
    import shutil
    backup_dir = os.path.join(BASE_DIR, '..', 'archive', 'pre-pint-scaling')
    os.makedirs(backup_dir, exist_ok=True)
    shutil.copy2(os.path.join(BASE_DIR, 'app.js'), os.path.join(backup_dir, 'app.js.bak'))
    shutil.copy2(os.path.join(BASE_DIR, 'swirl_master.db'), os.path.join(backup_dir, 'swirl_master.db.bak'))
    shutil.copy2(os.path.join(BASE_DIR, 'seed_recipes.json'), os.path.join(backup_dir, 'seed_recipes.json.bak'))
    print(f"\n💾 Backups saved to {backup_dir}/")

    # Apply
    print("\n🔧 Applying changes...")
    update_appjs(scaled)
    update_seed_json(scaled)
    update_database(scaled)

    print("\n✅ All done! All recipes now fit exactly 1 Ninja Swirl pint.")
    print("   Restart PM2: pm2 restart swirl-master")
