import json
import logging
import os
import re

from flask import flash, jsonify, redirect, render_template, request, session, url_for
from sqlalchemy.orm.attributes import flag_modified
from models import Action, Character, Detailer, Look, Outfit, Scene, Style, db
from services.file_io import get_available_loras
from services.job_queue import _enqueue_job, _make_finalize
from services.llm import call_character_mcp_tool, call_llm, load_prompt
from services.prompts import build_prompt
from services.sync import sync_characters
from services.workflow import _get_default_checkpoint, _prepare_workflow
from routes.shared import register_common_routes, apply_library_filters

logger = logging.getLogger('gaze')


def register_routes(app):
    register_common_routes(app, 'characters')

    @app.route('/')
    def index():
        characters, fav, nsfw = apply_library_filters(Character.query, Character)
        return render_template('index.html', characters=characters, favourite_filter=fav, nsfw_filter=nsfw)

    @app.route('/rescan', methods=['POST'])
    def rescan():
        sync_characters()
        flash('Database synced with character files.')
        return redirect(url_for('index'))

    @app.route('/character/<path:slug>')
    def detail(slug):
        character = Character.query.filter_by(slug=slug).first_or_404()
        all_outfits = Outfit.query.order_by(Outfit.name).all()
        outfit_map = {o.outfit_id: o for o in all_outfits}

        # Helper function for template to get outfit by ID
        def get_outfit_by_id(outfit_id):
            return outfit_map.get(outfit_id)

        # Load state from session
        preferences = session.get(f'prefs_{slug}')
        preview_image = session.get(f'preview_{slug}')
        extra_positive = session.get(f'extra_pos_{slug}', '')
        extra_negative = session.get(f'extra_neg_{slug}', '')

        return render_template('detail.html', character=character, preferences=preferences, preview_image=preview_image, all_outfits=all_outfits, outfit_map=outfit_map, get_outfit_by_id=get_outfit_by_id, extra_positive=extra_positive, extra_negative=extra_negative)

    @app.route('/character/<path:slug>/transfer', methods=['GET', 'POST'])
    def transfer_character(slug):
        character = Character.query.filter_by(slug=slug).first_or_404()

        if request.method == 'POST':
            target_type = request.form.get('target_type')
            new_name = request.form.get('new_name', '').strip()
            use_llm = request.form.get('use_llm') == 'on'

            if not new_name:
                flash('New name is required for transfer')
                return redirect(url_for('transfer_character', slug=slug))

            # Validate new name length and content
            if len(new_name) > 100:
                flash('New name must be 100 characters or less')
                return redirect(url_for('transfer_character', slug=slug))

            # Validate target type
            VALID_TARGET_TYPES = {'look', 'outfit', 'action', 'style', 'scene', 'detailer'}
            if target_type not in VALID_TARGET_TYPES:
                flash('Invalid target type')
                return redirect(url_for('transfer_character', slug=slug))

            # Generate new slug from name
            new_slug = re.sub(r'[^a-zA-Z0-9]+', '_', new_name.lower()).strip('_')
            safe_slug = re.sub(r'[^a-zA-Z0-9_]', '', new_slug)
            if not safe_slug:
                safe_slug = 'transferred'

            # Find available filename
            base_slug = safe_slug
            counter = 1
            target_dir = None
            model_class = None

            # Map target type to directory and model
            if target_type == 'look':
                target_dir = app.config['LOOKS_DIR']
                model_class = Look
                id_field = 'look_id'
            elif target_type == 'outfit':
                target_dir = app.config['CLOTHING_DIR']
                model_class = Outfit
                id_field = 'outfit_id'
            elif target_type == 'action':
                target_dir = app.config['ACTIONS_DIR']
                model_class = Action
                id_field = 'action_id'
            elif target_type == 'style':
                target_dir = app.config['STYLES_DIR']
                model_class = Style
                id_field = 'style_id'
            elif target_type == 'scene':
                target_dir = app.config['SCENES_DIR']
                model_class = Scene
                id_field = 'scene_id'
            elif target_type == 'detailer':
                target_dir = app.config['DETAILERS_DIR']
                model_class = Detailer
                id_field = 'detailer_id'
            else:
                flash('Invalid target type')
                return redirect(url_for('transfer_character', slug=slug))

            # Check for existing file
            while os.path.exists(os.path.join(target_dir, f"{safe_slug}.json")):
                safe_slug = f"{base_slug}_{counter}"
                counter += 1

            if use_llm:
                # Use LLM to regenerate JSON for new type
                try:
                    # Create prompt for LLM to convert character to target type
                    system_prompt = load_prompt('transfer_system.txt')
                    if not system_prompt:
                        system_prompt = f"""You are an AI assistant that converts character profiles to {target_type} profiles.

                        Convert the following character profile into a {target_type} profile.
                        A {target_type} should focus on {target_type}-specific details.
                        Keep the core identity but adapt it for the new context.
                        Return only valid JSON with no markdown formatting."""

                    # Prepare character data for LLM
                    char_summary = json.dumps(character.data, indent=2)
                    llm_prompt = f"""Convert this character profile to a {target_type} profile:

                    Original character name: {character.name}
                    Target {target_type} name: {new_name}

                    Character data:
                    {char_summary}

                    Create a new {target_type} JSON structure appropriate for {target_type}s."""

                    llm_response = call_llm(llm_prompt, system_prompt)

                    # Clean response
                    clean_json = llm_response.replace('```json', '').replace('```', '').strip()
                    new_data = json.loads(clean_json)

                    # Ensure required fields
                    new_data[f'{target_type}_id'] = safe_slug
                    new_data[f'{target_type}_name'] = new_name

                except Exception as e:
                    logger.exception("LLM transfer error: %s", e)
                    flash(f'Failed to generate {target_type} with AI: {e}')
                    return redirect(url_for('transfer_character', slug=slug))
            else:
                # Create blank template for target type
                new_data = {
                    f'{target_type}_id': safe_slug,
                    f'{target_type}_name': new_name,
                    'description': f'Transferred from character: {character.name}',
                    'tags': character.data.get('tags', []),
                    'lora': character.data.get('lora', {})
                }

            try:
                # Save new file
                file_path = os.path.join(target_dir, f"{safe_slug}.json")
                with open(file_path, 'w') as f:
                    json.dump(new_data, f, indent=2)

                # Create database entry
                new_entity = model_class(
                    **{id_field: safe_slug},
                    slug=safe_slug,
                    filename=f"{safe_slug}.json",
                    name=new_name,
                    data=new_data
                )
                db.session.add(new_entity)
                db.session.commit()

                flash(f'Successfully transferred to {target_type}: {new_name}')

                # Redirect to new entity's detail page
                if target_type == 'look':
                    return redirect(url_for('look_detail', slug=safe_slug))
                elif target_type == 'outfit':
                    return redirect(url_for('outfit_detail', slug=safe_slug))
                elif target_type == 'action':
                    return redirect(url_for('action_detail', slug=safe_slug))
                elif target_type == 'style':
                    return redirect(url_for('style_detail', slug=safe_slug))
                elif target_type == 'scene':
                    return redirect(url_for('scene_detail', slug=safe_slug))
                elif target_type == 'detailer':
                    return redirect(url_for('detailer_detail', slug=safe_slug))

            except Exception as e:
                logger.exception("Transfer save error: %s", e)
                flash(f'Failed to save transferred {target_type}: {e}')
                return redirect(url_for('transfer_character', slug=slug))

        # GET request - show transfer form
        return render_template('transfer.html', character=character)

    @app.route('/create', methods=['GET', 'POST'])
    def create_character():
        # Form data to preserve on errors
        form_data = {}

        if request.method == 'POST':
            name = request.form.get('name')
            slug = request.form.get('filename', '').strip()
            prompt = request.form.get('prompt', '')
            wiki_url = request.form.get('wiki_url', '').strip()
            use_llm = request.form.get('use_llm') == 'on'
            outfit_mode = request.form.get('outfit_mode', 'generate')  # 'generate', 'existing', 'none'
            existing_outfit_id = request.form.get('existing_outfit_id')

            # Store form data for re-rendering on error
            form_data = {
                'name': name,
                'filename': slug,
                'prompt': prompt,
                'wiki_url': wiki_url,
                'use_llm': use_llm,
                'outfit_mode': outfit_mode,
                'existing_outfit_id': existing_outfit_id
            }

            # Check for AJAX request
            is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest'

            # Auto-generate slug from name if not provided
            if not slug:
                slug = re.sub(r'[^a-zA-Z0-9]+', '_', name.lower()).strip('_')

            # Validate slug
            safe_slug = re.sub(r'[^a-zA-Z0-9_]', '', slug)
            if not safe_slug:
                safe_slug = 'character'

            # Find available filename (increment if exists)
            base_slug = safe_slug
            counter = 1
            while os.path.exists(os.path.join(app.config['CHARACTERS_DIR'], f"{safe_slug}.json")):
                safe_slug = f"{base_slug}_{counter}"
                counter += 1

            # Check if LLM generation is requested
            if use_llm:
                if not prompt:
                    error_msg = "Description is required when AI generation is enabled."
                    if is_ajax:
                        return jsonify({'error': error_msg}), 400
                    flash(error_msg)
                    return render_template('create.html', form_data=form_data, all_outfits=Outfit.query.order_by(Outfit.name).all())

                # Fetch reference data from wiki URL if provided
                wiki_reference = ''
                if wiki_url:
                    logger.info("Fetching character data from URL: %s", wiki_url)
                    wiki_data = call_character_mcp_tool('get_character_from_url', {
                        'url': wiki_url,
                        'name': name,
                    })
                    if wiki_data:
                        wiki_reference = f"\n\nReference data from wiki:\n{wiki_data}\n\nUse this reference to accurately describe the character's appearance, outfit, and features."
                        logger.info("Got wiki reference data (%d chars)", len(wiki_data))
                    else:
                        logger.warning("Failed to fetch wiki data from %s", wiki_url)

                # Step 1: Generate or select outfit first
                default_outfit_id = 'default'
                generated_outfit = None

                if outfit_mode == 'generate':
                    # Generate outfit with LLM
                    outfit_slug = f"{safe_slug}_outfit"
                    outfit_name = f"{name} - default"

                    outfit_prompt = f"""Generate an outfit for character "{name}".
The character is described as: {prompt}{wiki_reference}

Create an outfit JSON with wardrobe fields appropriate for this character."""

                    system_prompt = load_prompt('outfit_system.txt')
                    if not system_prompt:
                        error_msg = "Outfit system prompt file not found."
                        if is_ajax:
                            return jsonify({'error': error_msg}), 500
                        flash(error_msg)
                        return render_template('create.html', form_data=form_data, all_outfits=Outfit.query.order_by(Outfit.name).all())

                    try:
                        outfit_response = call_llm(outfit_prompt, system_prompt)
                        clean_outfit_json = outfit_response.replace('```json', '').replace('```', '').strip()
                        outfit_data = json.loads(clean_outfit_json)

                        # Enforce outfit IDs
                        outfit_data['outfit_id'] = outfit_slug
                        outfit_data['outfit_name'] = outfit_name

                        # Ensure required fields
                        if 'wardrobe' not in outfit_data:
                            outfit_data['wardrobe'] = {
                                "base": "",
                                "head": "",
                                "upper_body": "",
                                "lower_body": "",
                                "hands": "",
                                "feet": "",
                                "additional": ""
                            }
                        if 'lora' not in outfit_data:
                            outfit_data['lora'] = {
                                "lora_name": "",
                                "lora_weight": 0.8,
                                "lora_triggers": ""
                            }
                        if 'tags' not in outfit_data:
                            outfit_data['tags'] = []

                        # Save the outfit
                        outfit_path = os.path.join(app.config['CLOTHING_DIR'], f"{outfit_slug}.json")
                        with open(outfit_path, 'w') as f:
                            json.dump(outfit_data, f, indent=2)

                        # Create DB entry
                        generated_outfit = Outfit(
                            outfit_id=outfit_slug,
                            slug=outfit_slug,
                            name=outfit_name,
                            data=outfit_data
                        )
                        db.session.add(generated_outfit)
                        db.session.commit()

                        default_outfit_id = outfit_slug
                        logger.info("Generated outfit: %s for character %s", outfit_name, name)

                    except Exception as e:
                        logger.exception("Outfit generation error: %s", e)
                        # Fall back to default
                        default_outfit_id = 'default'

                elif outfit_mode == 'existing' and existing_outfit_id:
                    # Use selected existing outfit
                    default_outfit_id = existing_outfit_id
                else:
                    # Use default outfit
                    default_outfit_id = 'default'

                # Step 2: Generate character (without wardrobe section)
                char_prompt = f"""Generate a character named "{name}".
Description: {prompt}{wiki_reference}

Default Outfit: {default_outfit_id}

Create a character JSON with identity, styles, and defaults sections.
Do NOT include a wardrobe section - the outfit is handled separately."""

                system_prompt = load_prompt('character_system.txt')
                if not system_prompt:
                    error_msg = "Character system prompt file not found."
                    if is_ajax:
                        return jsonify({'error': error_msg}), 500
                    flash(error_msg)
                    return render_template('create.html', form_data=form_data, all_outfits=Outfit.query.order_by(Outfit.name).all())

                try:
                    llm_response = call_llm(char_prompt, system_prompt)

                    # Clean response (remove markdown if present)
                    clean_json = llm_response.replace('```json', '').replace('```', '').strip()
                    char_data = json.loads(clean_json)

                    # Enforce IDs
                    char_data['character_id'] = safe_slug
                    char_data['character_name'] = name

                    # Ensure outfit reference is set
                    if 'defaults' not in char_data:
                        char_data['defaults'] = {}
                    char_data['defaults']['outfit'] = default_outfit_id

                    # Remove any wardrobe section that LLM might have added
                    char_data.pop('wardrobe', None)

                except Exception as e:
                    logger.exception("LLM error: %s", e)
                    error_msg = f"Failed to generate character profile: {e}"
                    if is_ajax:
                        return jsonify({'error': error_msg}), 500
                    flash(error_msg)
                    return render_template('create.html', form_data=form_data, all_outfits=Outfit.query.order_by(Outfit.name).all())
            else:
                # Non-LLM: Create minimal character template
                char_data = {
                    "character_id": safe_slug,
                    "character_name": name,
                    "identity": {
                        "base": prompt,
                        "head": "",
                        "upper_body": "",
                        "lower_body": "",
                        "hands": "",
                        "feet": "",
                        "additional": ""
                    },
                    "defaults": {
                        "expression": "",
                        "pose": "",
                        "scene": "",
                        "outfit": existing_outfit_id if outfit_mode == 'existing' else 'default'
                    },
                    "styles": {
                        "aesthetic": "",
                        "primary_color": "",
                        "secondary_color": "",
                        "tertiary_color": ""
                    },
                    "lora": {
                        "lora_name": "",
                        "lora_weight": 1,
                        "lora_weight_min": 0.7,
                        "lora_weight_max": 1,
                        "lora_triggers": ""
                    },
                    "tags": []
                }

            try:
                # Save file
                file_path = os.path.join(app.config['CHARACTERS_DIR'], f"{safe_slug}.json")
                with open(file_path, 'w') as f:
                    json.dump(char_data, f, indent=2)

                # Add to DB
                new_char = Character(
                    character_id=safe_slug,
                    slug=safe_slug,
                    filename=f"{safe_slug}.json",
                    name=name,
                    data=char_data
                )

                # If outfit was generated, assign it to the character
                if outfit_mode == 'generate' and default_outfit_id != 'default':
                    new_char.assigned_outfit_ids = [default_outfit_id]
                    db.session.commit()

                db.session.add(new_char)
                db.session.commit()

                flash('Character created successfully!')
                return redirect(url_for('detail', slug=safe_slug))

            except Exception as e:
                logger.exception("Save error: %s", e)
                error_msg = f"Failed to create character: {e}"
                if is_ajax:
                    return jsonify({'error': error_msg}), 500
                flash(error_msg)
                return render_template('create.html', form_data=form_data, all_outfits=Outfit.query.order_by(Outfit.name).all())

        # GET request: show all outfits for the selector
        all_outfits = Outfit.query.order_by(Outfit.name).all()
        return render_template('create.html', form_data={}, all_outfits=all_outfits)

    @app.route('/character/<path:slug>/edit', methods=['GET', 'POST'])
    def edit_character(slug):
        character = Character.query.filter_by(slug=slug).first_or_404()
        loras = get_available_loras('characters')
        char_looks = Look.query.filter_by(character_id=character.character_id).order_by(Look.name).all()

        if request.method == 'POST':
            try:
                # 1. Update basic fields
                character.name = request.form.get('character_name')

                # 2. Rebuild the data dictionary
                new_data = character.data.copy()
                new_data['character_name'] = character.name

                # Update nested sections (non-wardrobe)
                for section in ['identity', 'defaults', 'styles', 'lora']:
                    if section in new_data:
                        for key in new_data[section]:
                            form_key = f"{section}_{key}"
                            if form_key in request.form:
                                val = request.form.get(form_key)
                                # Handle numeric weight
                                if key == 'lora_weight':
                                    try: val = float(val)
                                    except: val = 1.0
                                new_data[section][key] = val

                # LoRA weight randomization bounds (new fields not present in existing JSON)
                for bound in ['lora_weight_min', 'lora_weight_max']:
                    val_str = request.form.get(f'lora_{bound}', '').strip()
                    if val_str:
                        try:
                            new_data.setdefault('lora', {})[bound] = float(val_str)
                        except ValueError:
                            pass
                    else:
                        new_data.setdefault('lora', {}).pop(bound, None)

                # Handle wardrobe - support both nested and flat formats
                wardrobe = new_data.get('wardrobe', {})
                if 'default' in wardrobe and isinstance(wardrobe.get('default'), dict):
                    # New nested format - update each outfit
                    for outfit_name in wardrobe.keys():
                        for key in wardrobe[outfit_name].keys():
                            form_key = f"wardrobe_{outfit_name}_{key}"
                            if form_key in request.form:
                                wardrobe[outfit_name][key] = request.form.get(form_key)
                    new_data['wardrobe'] = wardrobe
                else:
                    # Legacy flat format
                    if 'wardrobe' in new_data:
                        for key in new_data['wardrobe'].keys():
                            form_key = f"wardrobe_{key}"
                            if form_key in request.form:
                                new_data['wardrobe'][key] = request.form.get(form_key)

                # Update structured tags
                new_data['tags'] = {
                    'origin_series': request.form.get('tag_origin_series', '').strip(),
                    'origin_type': request.form.get('tag_origin_type', '').strip(),
                    'nsfw': 'tag_nsfw' in request.form,
                }
                character.is_nsfw = new_data['tags']['nsfw']

                character.data = new_data
                flag_modified(character, "data")

                # 3. Write back to JSON file
                # Use the filename we stored during sync, or fallback to a sanitized ID
                char_file = character.filename or f"{re.sub(r'[^a-zA-Z0-9_]', '', character.character_id)}.json"
                file_path = os.path.join(app.config['CHARACTERS_DIR'], char_file)

                with open(file_path, 'w') as f:
                    json.dump(new_data, f, indent=2)

                db.session.commit()
                flash('Character profile updated successfully!')
                return redirect(url_for('detail', slug=slug))

            except Exception as e:
                logger.exception("Edit error: %s", e)
                flash(f"Error saving changes: {str(e)}")

        return render_template('edit.html', character=character, loras=loras, char_looks=char_looks)

    @app.route('/character/<path:slug>/outfit/switch', methods=['POST'])
    def switch_outfit(slug):
        """Switch the active outfit for a character."""
        character = Character.query.filter_by(slug=slug).first_or_404()
        outfit_id = request.form.get('outfit', 'default')

        # Get available outfits and validate
        available_outfits = character.get_available_outfits()
        available_ids = [o['outfit_id'] for o in available_outfits]

        if outfit_id in available_ids:
            character.active_outfit = outfit_id
            db.session.commit()
            outfit_name = next((o['name'] for o in available_outfits if o['outfit_id'] == outfit_id), outfit_id)
            flash(f'Switched to "{outfit_name}" outfit.')
        else:
            flash(f'Outfit "{outfit_id}" not found.', 'error')

        return redirect(url_for('detail', slug=slug))

    @app.route('/character/<path:slug>/outfit/assign', methods=['POST'])
    def assign_outfit(slug):
        """Assign an outfit from the Outfit table to this character."""
        character = Character.query.filter_by(slug=slug).first_or_404()
        outfit_id = request.form.get('outfit_id')

        if not outfit_id:
            flash('No outfit selected.', 'error')
            return redirect(url_for('detail', slug=slug))

        # Check if outfit exists
        outfit = Outfit.query.filter_by(outfit_id=outfit_id).first()
        if not outfit:
            flash(f'Outfit "{outfit_id}" not found.', 'error')
            return redirect(url_for('detail', slug=slug))

        # Assign outfit
        if character.assign_outfit(outfit_id):
            db.session.commit()
            flash(f'Assigned outfit "{outfit.name}" to {character.name}.')
        else:
            flash(f'Outfit "{outfit.name}" is already assigned.', 'warning')

        return redirect(url_for('detail', slug=slug))

    @app.route('/character/<path:slug>/outfit/unassign/<outfit_id>', methods=['POST'])
    def unassign_outfit(slug, outfit_id):
        """Unassign an outfit from this character."""
        character = Character.query.filter_by(slug=slug).first_or_404()

        if character.unassign_outfit(outfit_id):
            db.session.commit()
            flash('Outfit unassigned.')
        else:
            flash('Outfit was not assigned.', 'warning')

        return redirect(url_for('detail', slug=slug))

    @app.route('/character/<path:slug>/outfit/add', methods=['POST'])
    def add_outfit(slug):
        """Add a new outfit to a character."""
        character = Character.query.filter_by(slug=slug).first_or_404()
        outfit_name = request.form.get('outfit_name', '').strip()

        if not outfit_name:
            flash('Outfit name cannot be empty.', 'error')
            return redirect(url_for('edit_character', slug=slug))

        # Sanitize outfit name for use as key
        safe_name = re.sub(r'[^a-zA-Z0-9_]', '_', outfit_name.lower())

        # Get wardrobe data
        wardrobe = character.data.get('wardrobe', {})

        # Ensure wardrobe is in new nested format
        if 'default' not in wardrobe or not isinstance(wardrobe.get('default'), dict):
            # Convert legacy format
            wardrobe = {'default': wardrobe}

        # Check if outfit already exists
        if safe_name in wardrobe:
            flash(f'Outfit "{safe_name}" already exists.', 'error')
            return redirect(url_for('edit_character', slug=slug))

        # Create new outfit (copy from default as template)
        default_outfit = wardrobe.get('default', {
            'base': '', 'head': '', 'upper_body': '', 'lower_body': '',
            'hands': '', 'feet': '', 'additional': ''
        })
        wardrobe[safe_name] = default_outfit.copy()

        # Update character data
        character.data['wardrobe'] = wardrobe
        flag_modified(character, 'data')

        # Save to JSON file
        char_file = character.filename or f"{re.sub(r'[^a-zA-Z0-9_]', '', character.character_id)}.json"
        file_path = os.path.join(app.config['CHARACTERS_DIR'], char_file)
        with open(file_path, 'w') as f:
            json.dump(character.data, f, indent=2)

        db.session.commit()
        flash(f'Added new outfit "{safe_name}".')

        return redirect(url_for('edit_character', slug=slug))

    @app.route('/character/<path:slug>/outfit/delete', methods=['POST'])
    def delete_outfit(slug):
        """Delete an outfit from a character."""
        character = Character.query.filter_by(slug=slug).first_or_404()
        outfit_name = request.form.get('outfit', '')

        wardrobe = character.data.get('wardrobe', {})

        # Cannot delete default
        if outfit_name == 'default':
            flash('Cannot delete the default outfit.', 'error')
            return redirect(url_for('edit_character', slug=slug))

        if outfit_name not in wardrobe:
            flash(f'Outfit "{outfit_name}" not found.', 'error')
            return redirect(url_for('edit_character', slug=slug))

        # Delete outfit
        del wardrobe[outfit_name]
        character.data['wardrobe'] = wardrobe
        flag_modified(character, 'data')

        # Switch active outfit if deleted was active
        if character.active_outfit == outfit_name:
            character.active_outfit = 'default'

        # Save to JSON file
        char_file = character.filename or f"{re.sub(r'[^a-zA-Z0-9_]', '', character.character_id)}.json"
        file_path = os.path.join(app.config['CHARACTERS_DIR'], char_file)
        with open(file_path, 'w') as f:
            json.dump(character.data, f, indent=2)

        db.session.commit()
        flash(f'Deleted outfit "{outfit_name}".')

        return redirect(url_for('edit_character', slug=slug))

    @app.route('/character/<path:slug>/outfit/rename', methods=['POST'])
    def rename_outfit(slug):
        """Rename an outfit."""
        character = Character.query.filter_by(slug=slug).first_or_404()
        old_name = request.form.get('old_name', '')
        new_name = request.form.get('new_name', '').strip()

        if not new_name:
            flash('New name cannot be empty.', 'error')
            return redirect(url_for('edit_character', slug=slug))

        # Sanitize new name
        safe_name = re.sub(r'[^a-zA-Z0-9_]', '_', new_name.lower())

        wardrobe = character.data.get('wardrobe', {})

        if old_name not in wardrobe:
            flash(f'Outfit "{old_name}" not found.', 'error')
            return redirect(url_for('edit_character', slug=slug))

        if safe_name in wardrobe and safe_name != old_name:
            flash(f'Outfit "{safe_name}" already exists.', 'error')
            return redirect(url_for('edit_character', slug=slug))

        # Rename (copy to new key, delete old)
        wardrobe[safe_name] = wardrobe.pop(old_name)
        character.data['wardrobe'] = wardrobe
        flag_modified(character, 'data')

        # Update active outfit if renamed was active
        if character.active_outfit == old_name:
            character.active_outfit = safe_name

        # Save to JSON file
        char_file = character.filename or f"{re.sub(r'[^a-zA-Z0-9_]', '', character.character_id)}.json"
        file_path = os.path.join(app.config['CHARACTERS_DIR'], char_file)
        with open(file_path, 'w') as f:
            json.dump(character.data, f, indent=2)

        db.session.commit()
        flash(f'Renamed outfit "{old_name}" to "{safe_name}".')

        return redirect(url_for('edit_character', slug=slug))

    @app.route('/generate_missing', methods=['POST'])
    def generate_missing():
        missing = Character.query.filter(
            (Character.image_path == None) | (Character.image_path == '')
        ).order_by(Character.name).all()

        if not missing:
            flash("No characters missing cover images.")
            return redirect(url_for('index'))

        enqueued = 0
        for character in missing:
            try:
                with open('comfy_workflow.json', 'r') as f:
                    workflow = json.load(f)
                prompts = build_prompt(character.data, None, character.default_fields, character.active_outfit)
                ckpt_path, ckpt_data = _get_default_checkpoint()
                workflow = _prepare_workflow(workflow, character, prompts, checkpoint=ckpt_path, checkpoint_data=ckpt_data)

                _slug = character.slug
                _enqueue_job(f"{character.name} – cover", workflow, _make_finalize('characters', _slug, Character))
                enqueued += 1
            except Exception as e:
                logger.exception("Error queuing cover generation for %s: %s", character.name, e)

        flash(f"Queued {enqueued} cover image generation job{'s' if enqueued != 1 else ''}.")
        return redirect(url_for('index'))

    @app.route('/character/<path:slug>/generate', methods=['POST'])
    def generate_image(slug):
        character = Character.query.filter_by(slug=slug).first_or_404()

        try:
            # Get action type
            action = request.form.get('action', 'preview')

            # Get selected fields
            selected_fields = request.form.getlist('include_field')

            # Get additional prompts
            extra_positive = request.form.get('extra_positive', '').strip()
            extra_negative = request.form.get('extra_negative', '').strip()

            # Save preferences
            session[f'prefs_{slug}'] = selected_fields
            session[f'extra_pos_{slug}'] = extra_positive
            session[f'extra_neg_{slug}'] = extra_negative
            session.modified = True

            # Parse optional seed
            seed_val = request.form.get('seed', '').strip()
            fixed_seed = int(seed_val) if seed_val else None

            # Build workflow
            with open('comfy_workflow.json', 'r') as f:
                workflow = json.load(f)
            prompts = build_prompt(character.data, selected_fields, character.default_fields, character.active_outfit)

            if extra_positive:
                prompts["main"] = f"{prompts['main']}, {extra_positive}"

            ckpt_path, ckpt_data = _get_default_checkpoint()
            workflow = _prepare_workflow(workflow, character, prompts, custom_negative=extra_negative or None, checkpoint=ckpt_path, checkpoint_data=ckpt_data, fixed_seed=fixed_seed)

            label = f"{character.name} – {action}"
            job = _enqueue_job(label, workflow, _make_finalize('characters', slug, Character, action))
            if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
                return {'status': 'queued', 'job_id': job['id']}
            return redirect(url_for('detail', slug=slug))

        except Exception as e:
            logger.exception("Generation error: %s", e)
            if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
                return {'error': str(e)}, 500
            flash(f"Error during generation: {str(e)}")
            return redirect(url_for('detail', slug=slug))

