# Outfit & Looks Refactor Plan

## Table of Contents
1. [Bug Fixes](#bug-fixes)
2. [Generate Character from Look](#generate-character-from-look)
3. [Expanded Features: Multi-Assignment](#expanded-features-multi-assignment)
4. [Outfit Handling Refactor](#outfit-handling-refactor)
5. [Migration Script](#migration-script)
6. [Implementation Phases](#implementation-phases)

---

## Bug Fixes

### Issue: Preview Gallery Not Showing on Looks Detail Pages

**Root Cause:**
The `look_detail()` route in `app.py` does not scan for or pass `existing_previews` to the template, while all other resource detail routes (`outfit_detail`, `action_detail`, etc.) do.

**Fix Required:**
Update `look_detail()` in `app.py` to scan for existing preview images:

```python
@app.route('/look/<path:slug>')
def look_detail(slug):
    look = Look.query.filter_by(slug=slug).first_or_404()
    characters = Character.query.order_by(Character.name).all()

    # Pre-select the linked character if set
    preferences = session.get(f'prefs_look_{slug}')
    preview_image = session.get(f'preview_look_{slug}')
    selected_character = session.get(f'char_look_{slug}', look.character_id or '')

    # FIX: Add existing_previews scanning (matching other resource routes)
    upload_folder = current_app.config.get('UPLOAD_FOLDER', 'static/uploads')
    preview_dir = os.path.join(upload_folder, 'looks', slug)
    existing_previews = []
    if os.path.isdir(preview_dir):
        for f in os.listdir(preview_dir):
            if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp')):
                existing_previews.append(f'looks/{slug}/{f}')
    existing_previews.sort()

    return render_template('looks/detail.html', look=look, characters=characters,
                           preferences=preferences, preview_image=preview_image,
                           selected_character=selected_character,
                           existing_previews=existing_previews)  # <-- ADD THIS
```

---

## Generate Character from Look

### Overview
Allow users to generate a character JSON using an existing look as input, similar to the resource transfer feature but without moving or removing any files. The look's LoRA will be automatically assigned to the new character.

### Backend Implementation

**New Route:** `POST /look/<slug>/generate_character`

```python
@app.route('/look/<slug>/generate_character', methods=['POST'])
def generate_character_from_look(slug):
    """Generate a character JSON using a look as the base."""
    look = Look.query.filter_by(slug=slug).first_or_404()
    
    # Get or validate inputs
    character_name = request.form.get('character_name', look.look_name)
    use_llm = request.form.get('use_llm') == 'on'
    
    # Auto-generate slug
    character_slug = re.sub(r'[^a-zA-Z0-9]+', '_', character_name.lower()).strip('_')
    character_slug = re.sub(r'[^a-zA-Z0-9_]', '', character_slug)
    
    # Find available filename
    base_slug = character_slug
    counter = 1
    while os.path.exists(os.path.join(CHARACTERS_DIR, f"{character_slug}.json")):
        character_slug = f"{base_slug}_{counter}"
        counter += 1
    
    if use_llm:
        # Use LLM to generate character from look context
        prompt = f"""Generate a character based on this look description:

Look Name: {look.look_name}
Positive Prompt: {look.data.get('positive', '')}
Negative Prompt: {look.data.get('negative', '')}
Tags: {', '.join(look.data.get('tags', []))}
LoRA Triggers: {look.data.get('lora', {}).get('lora_triggers', '')}

Create a complete character JSON with identity, styles, and appropriate wardrobe fields.
The character should match the visual style described in the look."""
        
        # Call LLM generation (reuse existing LLM infrastructure)
        character_data = generate_character_with_llm(prompt, character_name, character_slug)
    else:
        # Create minimal character template
        character_data = {
            "character_id": character_slug,
            "character_name": character_name,
            "identity": {
                "base_specs": look.data.get('lora', {}).get('lora_triggers', ''),
                "hair": "",
                "eyes": "",
                "hands": "",
                "arms": "",
                "torso": "",
                "pelvis": "",
                "legs": "",
                "feet": "",
                "extra": ""
            },
            "defaults": {
                "expression": "",
                "pose": "",
                "scene": ""
            },
            "wardrobe": {
                "full_body": "",
                "headwear": "",
                "top": "",
                "bottom": "",
                "legwear": "",
                "footwear": "",
                "hands": "",
                "accessories": ""
            },
            "styles": {
                "aesthetic": "",
                "primary_color": "",
                "secondary_color": "",
                "tertiary_color": ""
            },
            "lora": look.data.get('lora', {}),  # <-- Auto-assign look's LoRA
            "tags": look.data.get('tags', [])
        }
    
    # Save character JSON
    char_path = os.path.join(CHARACTERS_DIR, f"{character_slug}.json")
    with open(char_path, 'w') as f:
        json.dump(character_data, f, indent=2)
    
    # Create DB entry
    character = Character(
        character_id=character_slug,
        slug=character_slug,
        name=character_name,
        data=character_data
    )
    db.session.add(character)
    db.session.commit()
    
    # Link the look to this character
    look.character_id = character_slug
    db.session.commit()
    
    flash(f'Character "{character_name}" created from look!', 'success')
    return redirect(url_for('detail', slug=character_slug))
```

### Frontend Implementation

**Update `templates/looks/detail.html`:**

Add a "Generate Character" button in the header actions:

```html
<!-- Add to header near other action buttons -->
<button type="button" class="btn btn-accent" data-bs-toggle="modal" data-bs-target="#generateCharModal">
  <i class="bi bi-person-plus"></i> Generate Character
</button>

<!-- Modal for Generate Character -->
<div class="modal fade" id="generateCharModal" tabindex="-1">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title">Generate Character from Look</h5>
        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
      </div>
      <form method="POST" action="{{ url_for('generate_character_from_look', slug=look.slug) }}">
        <div class="modal-body">
          <div class="mb-3">
            <label class="form-label">Character Name</label>
            <input type="text" class="form-control" name="character_name" 
                   value="{{ look.look_name }}" required>
          </div>
          <div class="form-check">
            <input class="form-check-input" type="checkbox" name="use_llm" id="use_llm" checked>
            <label class="form-check-label" for="use_llm">
              Use LLM to generate detailed character
            </label>
          </div>
          <div class="form-text text-muted">
            The look's LoRA will be automatically assigned to the new character.
          </div>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
          <button type="submit" class="btn btn-primary">Generate Character</button>
        </div>
      </form>
    </div>
  </div>
</div>
```

---

## Expanded Features: Multi-Assignment

### Allow a Look to be Assigned to More Than One Character

**Current State:**
- Look has a single `character_id` field (string)
- Template shows single character selector

**New State:**
- Look has `character_ids` field (list of strings)
- Template allows multiple character selection

#### Database/Model Changes

**Update `Look` model in `models.py`:**

```python
class Look(ResourceMixin, db.Model):
    __tablename__ = 'looks'
    
    look_id = db.Column(db.String, primary_key=True)
    slug = db.Column(db.String, unique=True, nullable=False)
    name = db.Column(db.String, nullable=False)
    data = db.Column(db.JSON)
    image_path = db.Column(db.String)
    default_fields = db.Column(db.JSON)
    
    # OLD: Single character link
    # character_id = db.Column(db.String, db.ForeignKey('characters.character_id'))
    # character = db.relationship('Character', back_populates='looks')
    
    # NEW: Multiple character links
    character_ids = db.Column(db.JSON, default=list)  # List of character_ids
    
    def get_linked_characters(self):
        """Get all characters linked to this look."""
        if not self.character_ids:
            return []
        return Character.query.filter(Character.character_id.in_(self.character_ids)).all()
    
    def add_character(self, character_id):
        """Link a character to this look."""
        if not self.character_ids:
            self.character_ids = []
        if character_id not in self.character_ids:
            self.character_ids.append(character_id)
    
    def remove_character(self, character_id):
        """Unlink a character from this look."""
        if self.character_ids and character_id in self.character_ids:
            self.character_ids.remove(character_id)
```

**Migration for existing data:**
```python
# One-time migration: convert character_id to character_ids list
for look in Look.query.all():
    if look.character_id and not look.character_ids:
        look.character_ids = [look.character_id]
        look.character_id = None  # Clear old field
db.session.commit()
```

#### UI Changes for Multi-Character Assignment

**Update `templates/looks/detail.html`:**

Replace single character selector with multi-select:

```html
<!-- OLD: Single character dropdown -->
{# <select name="character_id" class="form-select">
    <option value="">-- Random Character --</option>
    {% for char in characters %}
    <option value="{{ char.character_id }}" {% if char.character_id == selected_character %}selected{% endif %}>
        {{ char.name }}
    </option>
    {% endfor %}
</select> #}

<!-- NEW: Multi-character selector with checkboxes -->
<div class="card">
  <div class="card-header">
    <span>Linked Characters</span>
    <small class="text-muted">(Check to link this look)</small>
  </div>
  <div class="card-body" style="max-height: 300px; overflow-y: auto;">
    {% for char in characters %}
    <div class="form-check">
      <input class="form-check-input" type="checkbox" name="character_ids" 
             value="{{ char.character_id }}" id="char_{{ char.character_id }}"
             {% if char.character_id in look.character_ids %}checked{% endif %}>
      <label class="form-check-label" for="char_{{ char.character_id }}">
        {{ char.name }}
      </label>
    </div>
    {% endfor %}
  </div>
</div>
```

---

### Allow Outfits to be Assigned to Characters as Additional Wardrobe Items

**New Relationship:** Many-to-many between Characters and Outfits

**Implementation Options:**

**Option A: JSON Array in Character (Simplest)**
```python
class Character(db.Model):
    # ... existing fields ...
    assigned_outfit_ids = db.Column(db.JSON, default=list)  # List of outfit_ids
    default_outfit_id = db.Column(db.String, default='default')  # Always have a default
```

**Option B: Association Table (More Robust)**
```python
character_outfits = db.Table('character_outfits',
    db.Column('character_id', db.String, db.ForeignKey('characters.character_id')),
    db.Column('outfit_id', db.String, db.ForeignKey('outfits.outfit_id')),
    db.Column('is_default', db.Boolean, default=False)
)
```

**Recommended: Option A** for simplicity and alignment with existing JSON-heavy architecture.

#### Character Model Updates

```python
class Character(db.Model):
    character_id = db.Column(db.String, primary_key=True)
    slug = db.Column(db.String, unique=True, nullable=False)
    name = db.Column(db.String, nullable=False)
    data = db.Column(db.JSON)
    active_outfit = db.Column(db.String, default='default')
    
    # NEW: List of assigned outfit IDs (empty list means only default)
    assigned_outfit_ids = db.Column(db.JSON, default=list)
    
    def get_available_outfits(self):
        """Get all outfits available to this character (assigned + default)."""
        outfit_ids = ['default']  # Always include default
        if self.assigned_outfit_ids:
            outfit_ids.extend(self.assigned_outfit_ids)
        
        # Fetch outfit objects from DB
        outfits = Outfit.query.filter(Outfit.outfit_id.in_(outfit_ids)).all()
        
        # Sort so default is first
        outfits.sort(key=lambda o: (o.outfit_id != 'default', o.outfit_name))
        return outfits
    
    def get_active_wardrobe(self):
        """Get wardrobe data from the active outfit."""
        if self.active_outfit and self.active_outfit != 'default':
            outfit = Outfit.query.filter_by(outfit_id=self.active_outfit).first()
            if outfit and outfit.data.get('wardrobe'):
                return outfit.data['wardrobe']
        
        # Fallback to legacy embedded wardrobe or empty
        return self.data.get('wardrobe', {}) if self.data else {}
    
    def assign_outfit(self, outfit_id):
        """Assign an outfit to this character."""
        if not self.assigned_outfit_ids:
            self.assigned_outfit_ids = []
        if outfit_id not in self.assigned_outfit_ids:
            self.assigned_outfit_ids.append(outfit_id)
    
    def unassign_outfit(self, outfit_id):
        """Remove an outfit assignment."""
        if self.assigned_outfit_ids and outfit_id in self.assigned_outfit_ids:
            self.assigned_outfit_ids.remove(outfit_id)
            # Reset to default if we just removed the active outfit
            if self.active_outfit == outfit_id:
                self.active_outfit = 'default'
```

#### UI for Outfit Assignment

**Update `templates/detail.html` (character detail):**

Add an outfit management section:

```html
<!-- Outfit Management Section -->
<div class="card mt-4">
  <div class="card-header">
    <span>Wardrobe</span>
  </div>
  <div class="card-body">
    <!-- Active Outfit Selector -->
    <form method="POST" action="{{ url_for('switch_outfit', slug=character.slug) }}" class="mb-3">
      <label class="form-label">Active Outfit</label>
      <div class="input-group">
        <select name="outfit_id" class="form-select">
          {% for outfit in character.get_available_outfits() %}
          <option value="{{ outfit.outfit_id }}" 
                  {% if outfit.outfit_id == character.active_outfit %}selected{% endif %}>
            {{ outfit.outfit_name }}
            {% if outfit.outfit_id == 'default' %}(Default){% endif %}
          </option>
          {% endfor %}
        </select>
        <button type="submit" class="btn btn-primary">Switch</button>
      </div>
    </form>
    
    <!-- Assign New Outfit -->
    <form method="POST" action="{{ url_for('assign_outfit', slug=character.slug) }}">
      <label class="form-label">Add Outfit to Wardrobe</label>
      <div class="input-group">
        <select name="outfit_id" class="form-select">
          <option value="">-- Select Outfit --</option>
          {% for outfit in all_outfits %}
            {% if outfit.outfit_id not in character.assigned_outfit_ids and outfit.outfit_id != 'default' %}
            <option value="{{ outfit.outfit_id }}">{{ outfit.outfit_name }}</option>
            {% endif %}
          {% endfor %}
        </select>
        <button type="submit" class="btn btn-success">Assign</button>
      </div>
    </form>
    
    <!-- Currently Assigned -->
    {% if character.assigned_outfit_ids %}
    <div class="mt-3">
      <label class="form-label">Assigned Outfits</label>
      <div class="d-flex flex-wrap gap-2">
        {% for outfit_id in character.assigned_outfit_ids %}
          {% set outfit = get_outfit_by_id(outfit_id) %}
          {% if outfit %}
          <span class="badge bg-secondary">
            {{ outfit.outfit_name }}
            <a href="{{ url_for('unassign_outfit', slug=character.slug, outfit_id=outfit_id) }}" 
               class="text-white ms-1" style="text-decoration: none;">&times;</a>
          </span>
          {% endif %}
        {% endfor %}
      </div>
    </div>
    {% endif %}
  </div>
</div>
```

---

## Outfit Handling Refactor

### Overview
Character JSON files will no longer contain built-in wardrobe items. Instead, they will reference external outfit files. This requires changes to:
1. Character JSON schema
2. Character creation flow
3. Prompt building logic

### New Character JSON Schema

**OLD Structure:**
```json
{
  "character_id": "aerith_gainsborough",
  "character_name": "Aerith Gainsborough",
  "identity": { ... },
  "wardrobe": {
    "full_body": "pink_dress, jacket",
    "top": "...",
    ...
  },
  "styles": { ... },
  "lora": { ... }
}
```

**NEW Structure:**
```json
{
  "character_id": "aerith_gainsborough",
  "character_name": "Aerith Gainsborough",
  "identity": { ... },
  "styles": { ... },
  "lora": { ... },
  "tags": [...],
  "defaults": {
    "expression": "",
    "pose": "",
    "scene": "",
    "outfit": "aerith_gainsborough - default"  // Reference to outfit file
  }
}
```

The `wardrobe` section is removed from the character JSON. Wardrobe data now lives exclusively in outfit files in `data/clothing/`.

### Prompt Building Refactor

**Current Prompt Building (in `_prepare_workflow` or similar):**
```python
# Build wardrobe from character's embedded wardrobe
wardrobe_data = character.data.get('wardrobe', {})
wardrobe_parts = [v for k, v in wardrobe_data.items() if v and include_field(f'wardrobe::{k}')]
if wardrobe_parts:
    prompt_parts.append(', '.join(wardrobe_parts))
```

**New Prompt Building:**
```python
def get_outfit_wardrobe(character, outfit_id=None):
    """
    Get wardrobe data from the referenced outfit file.
    Falls back to default outfit if outfit_id not found.
    """
    if outfit_id is None:
        outfit_id = character.data.get('defaults', {}).get('outfit', 'default')
    
    # Try to load outfit from DB/file
    outfit = Outfit.query.filter_by(outfit_id=outfit_id).first()
    
    if outfit and outfit.data:
        return outfit.data.get('wardrobe', {})
    
    # Fallback to default outfit
    default_outfit = Outfit.query.filter_by(outfit_id='default').first()
    if default_outfit and default_outfit.data:
        return default_outfit.data.get('wardrobe', {})
    
    # Final fallback: empty wardrobe
    return {}

# In prompt building:
wardrobe_data = get_outfit_wardrobe(character, outfit_id)
wardrobe_parts = [v for k, v in wardrobe_data.items() if v and include_field(f'wardrobe::{k}')]
if wardrobe_parts:
    prompt_parts.append(', '.join(wardrobe_parts))
```

### Character Creation Refactor (Two-Step LLM Flow)

When creating a character, we now need to either:
1. Generate an outfit first, then the character, OR
2. Assign an existing outfit

**New Character Creation Flow:**

```python
@app.route('/create', methods=['GET', 'POST'])
def create_character():
    if request.method == 'POST':
        name = request.form.get('name')
        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')
        
        # ... slug generation and validation ...
        
        if use_llm:
            # Step 1: Generate outfit first (if requested)
            if outfit_mode == 'generate':
                outfit_prompt = f"""Generate an outfit for character "{name}".
The character is described as: {prompt}

Create an outfit JSON with wardrobe fields appropriate for this character."""
                
                outfit_data = generate_outfit_with_llm(outfit_prompt, outfit_slug, name)
                
                # Save the outfit
                outfit_path = os.path.join(CLOTHING_DIR, f"{outfit_slug}.json")
                with open(outfit_path, 'w') as f:
                    json.dump(outfit_data, f, indent=2)
                
                outfit = Outfit(
                    outfit_id=outfit_slug,
                    slug=outfit_slug,
                    name=f"{name} - default",
                    data=outfit_data
                )
                db.session.add(outfit)
                db.session.commit()
                
                default_outfit_id = outfit_slug
            
            elif outfit_mode == 'existing':
                default_outfit_id = existing_outfit_id
            else:
                default_outfit_id = 'default'
            
            # Step 2: Generate character (without wardrobe section)
            char_prompt = f"""Generate a character named "{name}".
Description: {prompt}

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."""
            
            character_data = generate_character_with_llm(char_prompt, name, slug)
            
            # Ensure outfit reference is set
            if 'defaults' not in character_data:
                character_data['defaults'] = {}
            character_data['defaults']['outfit'] = default_outfit_id
            
            # Remove any wardrobe section that LLM might have added
            character_data.pop('wardrobe', None)
            
        else:
            # Non-LLM: Create minimal character
            character_data = {
                "character_id": slug,
                "character_name": name,
                "identity": {
                    "base_specs": prompt,
                    "hair": "",
                    "eyes": "",
                    "hands": "",
                    "arms": "",
                    "torso": "",
                    "pelvis": "",
                    "legs": "",
                    "feet": "",
                    "extra": ""
                },
                "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": []
            }
        
        # ... save character ...
        
        # If outfit was generated, assign it to the character
        if outfit_mode == 'generate' and default_outfit_id != 'default':
            character.assigned_outfit_ids = [default_outfit_id]
            db.session.commit()
```

**Update `templates/create.html`:**

```html
<!-- Add to character creation form -->
<div class="mb-3">
  <label class="form-label">Outfit</label>
  <div class="btn-group w-100" role="group">
    <input type="radio" class="btn-check" name="outfit_mode" id="outfit_generate" value="generate" checked>
    <label class="btn btn-outline-primary" for="outfit_generate">Generate New</label>
    
    <input type="radio" class="btn-check" name="outfit_mode" id="outfit_existing" value="existing">
    <label class="btn btn-outline-primary" for="outfit_existing">Use Existing</label>
    
    <input type="radio" class="btn-check" name="outfit_mode" id="outfit_none" value="none">
    <label class="btn btn-outline-primary" for="outfit_none">Default Only</label>
  </div>
</div>

<!-- Existing outfit selector (shown when 'existing' selected) -->
<div class="mb-3" id="existing_outfit_selector" style="display: none;">
  <select name="existing_outfit_id" class="form-select">
    <option value="">-- Select Outfit --</option>
    {% for outfit in all_outfits %}
    <option value="{{ outfit.outfit_id }}">{{ outfit.outfit_name }}</option>
    {% endfor %}
  </select>
</div>

<script>
document.querySelectorAll('input[name="outfit_mode"]').forEach(radio => {
    radio.addEventListener('change', () => {
        document.getElementById('existing_outfit_selector').style.display = 
            document.getElementById('outfit_existing').checked ? 'block' : 'none';
    });
});
</script>
```

---

## Migration Script

### Extract Existing Wardrobes to Outfit Files

Before the refactor can go live, we need to extract existing wardrobe data from character JSON files into the outfits directory.

**Script: `tools/migrate_wardrobes.py`**

```python
#!/usr/bin/env python3
"""
Migration script: Extract wardrobes from character JSON files into outfit files.

Usage:
    python tools/migrate_wardrobes.py [--dry-run]

This will:
1. Read each character JSON in data/characters/
2. Extract the wardrobe section
3. Create a new outfit file: data/clothing/{char_name} - default.json
4. Update the character JSON to remove wardrobe and add defaults.outfit reference
"""

import os
import sys
import json
import re
from pathlib import Path

# Add parent to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))

CHARACTERS_DIR = Path('data/characters')
CLOTHING_DIR = Path('data/clothing')

def sanitize_filename(name):
    """Convert name to safe filename."""
    # Remove/replace unsafe characters
    safe = re.sub(r'[<>:"/\\|?*]', '', name)
    safe = safe.strip()
    return safe

def migrate_character(char_path, dry_run=True):
    """Migrate a single character file."""
    with open(char_path, 'r') as f:
        data = json.load(f)
    
    char_id = data.get('character_id', '')
    char_name = data.get('character_name', char_id)
    wardrobe = data.get('wardrobe', {})
    
    # Skip if no wardrobe or wardrobe is empty
    if not wardrobe or not any(wardrobe.values()):
        print(f"  Skipping {char_name} - no wardrobe data")
        return
    
    # Generate outfit filename
    outfit_name = f"{char_name} - default"
    outfit_slug = sanitize_filename(outfit_name).lower().replace(' ', '_')
    outfit_filename = f"{outfit_slug}.json"
    outfit_path = CLOTHING_DIR / outfit_filename
    
    # Handle duplicates
    counter = 1
    while outfit_path.exists():
        outfit_slug = f"{sanitize_filename(char_name).lower().replace(' ', '_')}_{counter}_default"
        outfit_filename = f"{outfit_slug}.json"
        outfit_path = CLOTHING_DIR / outfit_filename
        counter += 1
    
    # Create outfit data
    outfit_data = {
        "outfit_id": outfit_slug,
        "outfit_name": f"{char_name} - default",
        "wardrobe": wardrobe,
        "lora": {
            "lora_name": "",
            "lora_weight": 0.8,
            "lora_triggers": "",
            "lora_weight_min": 0.8,
            "lora_weight_max": 0.8
        },
        "tags": ["default", char_name.lower().replace(' ', '_')]
    }
    
    # Copy LoRA from character if present
    char_lora = data.get('lora', {})
    if char_lora and char_lora.get('lora_name'):
        outfit_data['lora'] = char_lora
    
    print(f"  Creating outfit: {outfit_filename}")
    print(f"    Wardrobe fields: {list(wardrobe.keys())}")
    
    if not dry_run:
        # Write outfit file
        with open(outfit_path, 'w') as f:
            json.dump(outfit_data, f, indent=2)
        
        # Update character JSON
        # Remove wardrobe section
        data.pop('wardrobe', None)
        
        # Ensure defaults exists and add outfit reference
        if 'defaults' not in data:
            data['defaults'] = {}
        data['defaults']['outfit'] = outfit_slug
        
        # Write updated character file
        with open(char_path, 'w') as f:
            json.dump(data, f, indent=2)
        
        print(f"    Updated character file")
    
    return outfit_slug

def main():
    import argparse
    parser = argparse.ArgumentParser(description='Migrate character wardrobes to outfit files')
    parser.add_argument('--dry-run', action='store_true', 
                        help='Show what would be done without making changes')
    args = parser.parse_args()
    
    if args.dry_run:
        print("=== DRY RUN MODE (no files will be modified) ===\n")
    
    # Ensure clothing directory exists
    CLOTHING_DIR.mkdir(parents=True, exist_ok=True)
    
    # Process all character files
    char_files = list(CHARACTERS_DIR.glob('*.json'))
    print(f"Found {len(char_files)} character files")
    print()
    
    migrated = 0
    skipped = 0
    
    for char_path in sorted(char_files):
        print(f"Processing: {char_path.name}")
        result = migrate_character(char_path, dry_run=args.dry_run)
        if result:
            migrated += 1
        else:
            skipped += 1
    
    print()
    print(f"Migration complete:")
    print(f"  Migrated: {migrated}")
    print(f"  Skipped (no wardrobe): {skipped}")
    
    if args.dry_run:
        print()
        print("This was a dry run. Run without --dry-run to apply changes.")

if __name__ == '__main__':
    main()
```

**Example Output:**
```
=== DRY RUN MODE (no files will be modified) ===

Found 45 character files

Processing: 2b.json
  Creating outfit: 2b_-_default.json
    Wardrobe fields: ['full_body', 'headwear', 'top', 'bottom', 'legwear', 'footwear', 'hands', 'accessories']
Processing: aerith_gainsborough.json
  Creating outfit: aerith_gainsborough_-_default.json
    Wardrobe fields: ['full_body', 'headwear', 'top', 'bottom', 'legwear', 'footwear', 'hands', 'accessories']
Processing: barret_wallace.json
  Skipping Barret Wallace - no wardrobe data

Migration complete:
  Migrated: 42
  Skipped (no wardrobe): 3
```

---

## Implementation Phases

### Phase 1: Bug Fix & Foundation (Immediate)
- [ ] Fix `look_detail()` to pass `existing_previews` (5 min fix)
- [ ] Create migration script `tools/migrate_wardrobes.py`
- [ ] Run migration script to extract existing wardrobes

### Phase 2: Character Generation from Look
- [ ] Add `generate_character_from_look` route to `app.py`
- [ ] Add modal UI to `templates/looks/detail.html`
- [ ] Test with both LLM and non-LLM paths

### Phase 3: Multi-Character Look Assignment
- [ ] Add `character_ids` JSON column to `Look` model
- [ ] Create migration to convert `character_id` → `character_ids`
- [ ] Update `look_detail` route to handle multiple characters
- [ ] Update `templates/looks/detail.html` with multi-select UI
- [ ] Update look generation logic to respect all linked characters

### Phase 4: Outfit-to-Character Assignment
- [ ] Add `assigned_outfit_ids` and `default_outfit_id` to `Character` model
- [ ] Add `get_available_outfits()` and `get_active_wardrobe()` methods
- [ ] Create routes: `assign_outfit`, `unassign_outfit`, `switch_outfit`
- [ ] Update `templates/detail.html` with outfit management UI
- [ ] Update outfit detail page to show linked characters

### Phase 5: Outfit Handling Refactor
- [ ] Update character creation to support two-step LLM flow
- [ ] Update `templates/create.html` with outfit mode selector
- [ ] Refactor `_prepare_workflow` / prompt building to pull from outfit files
- [ ] Create default outfit file if it doesn't exist
- [ ] Update character detail page to show wardrobe from external outfit

### Phase 6: Testing & Cleanup
- [ ] Verify all existing characters work with new outfit system
- [ ] Test character creation with all outfit modes
- [ ] Test look generation with multiple linked characters
- [ ] Clean up any legacy code referencing embedded wardrobes
- [ ] Update documentation (CLAUDE.md, DEVELOPMENT_GUIDE.md)

---

## Database Migration Summary

### Migration 1: Look Multi-Character Support
```sql
-- Add new column
ALTER TABLE looks ADD COLUMN character_ids JSON;

-- Migrate existing data
UPDATE looks SET character_ids = JSON_ARRAY(character_id) WHERE character_id IS NOT NULL;
UPDATE looks SET character_ids = JSON_ARRAY() WHERE character_id IS NULL;

-- Optionally drop old column after migration is verified
-- ALTER TABLE looks DROP COLUMN character_id;
```

### Migration 2: Character Outfit Assignment
```sql
-- Add new columns
ALTER TABLE characters ADD COLUMN assigned_outfit_ids JSON DEFAULT '[]';
ALTER TABLE characters ADD COLUMN default_outfit_id VARCHAR(255) DEFAULT 'default';
```

### Migration 3: Ensure Default Outfit Exists
```python
# Ensure there's a default outfit in the database
default_outfit = Outfit.query.filter_by(outfit_id='default').first()
if not default_outfit:
    default_outfit = Outfit(
        outfit_id='default',
        slug='default',
        name='Default Outfit',
        data={'wardrobe': {}, 'lora': {}, 'tags': ['default']}
    )
    db.session.add(default_outfit)
    db.session.commit()
```

---

## Files to Modify

| File | Changes |
|------|---------|
| `app.py` | Add `existing_previews` to `look_detail()`, add `generate_character_from_look` route, update `create_character` for two-step flow, update prompt building |
| `models.py` | Add `character_ids` to `Look`, add `assigned_outfit_ids`/`default_outfit_id` to `Character`, add helper methods |
| `templates/looks/detail.html` | Add generate character modal, update to multi-character selector |
| `templates/detail.html` | Add outfit management section |
| `templates/create.html` | Add outfit mode selector (generate/existing/none) |
| `templates/outfits/detail.html` | Show linked characters |
| `tools/migrate_wardrobes.py` | New migration script |
| `CLAUDE.md` | Update character JSON schema documentation |
| `DEVELOPMENT_GUIDE.md` | Update development guidelines |

---

## Backward Compatibility Notes

1. **Migration must be run first** - All existing characters need wardrobes extracted before the new code can read them
2. **Default outfit** - All characters must have a default outfit reference (even if empty)
3. **Character data structure** - The JSON structure change is breaking; ensure migration script handles all edge cases
4. **Look character_id** - Keep the old field temporarily during migration, remove after `character_ids` is populated

---

## Testing Checklist

- [ ] Look preview gallery displays correctly
- [ ] Generate character from look works (LLM mode)
- [ ] Generate character from look works (non-LLM mode)
- [ ] Look's LoRA is auto-assigned to new character
- [ ] Multiple characters can be linked to a look
- [ ] Look generation respects all linked characters
- [ ] Existing outfits can be assigned to characters
- [ ] Characters can switch between assigned outfits
- [ ] Character creation with generated outfit works
- [ ] Character creation with existing outfit works
- [ ] Character creation with default outfit works
- [ ] Prompt building pulls from correct outfit file
- [ ] Fallback to default outfit if referenced outfit missing
- [ ] Migration script extracts all wardrobes correctly
- [ ] No data loss during migration
