import json
import os
import re
import logging

from flask import render_template, request, redirect, url_for, flash, session, current_app
from werkzeug.utils import secure_filename
from sqlalchemy.orm.attributes import flag_modified

from models import db, Character, Outfit, Action, Style, Scene, Detailer, Checkpoint, Settings, Look
from services.workflow import _prepare_workflow, _get_default_checkpoint
from services.job_queue import _enqueue_job, _make_finalize, _enqueue_task
from services.prompts import build_prompt, _resolve_character, _ensure_character_fields, _append_background
from services.sync import sync_outfits
from services.file_io import get_available_loras, _count_outfit_lora_assignments
from utils import allowed_file, _LORA_DEFAULTS, clean_html_text
from services.llm import load_prompt, call_llm
from routes.shared import register_common_routes, apply_library_filters

logger = logging.getLogger('gaze')


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

    @app.route('/outfits')
    def outfits_index():
        outfits, fav, nsfw = apply_library_filters(Outfit.query, Outfit)
        lora_assignments = _count_outfit_lora_assignments()
        return render_template('outfits/index.html', outfits=outfits, lora_assignments=lora_assignments, favourite_filter=fav, nsfw_filter=nsfw)

    @app.route('/outfits/rescan', methods=['POST'])
    def rescan_outfits():
        sync_outfits()
        flash('Database synced with outfit files.')
        return redirect(url_for('outfits_index'))

    @app.route('/outfits/bulk_create', methods=['POST'])
    def bulk_create_outfits_from_loras():
        _s = Settings.query.first()
        clothing_lora_dir = ((_s.lora_dir_outfits if _s else None) or '/ImageModels/lora/Illustrious/Clothing').rstrip('/')
        _lora_subfolder = os.path.basename(clothing_lora_dir)
        if not os.path.exists(clothing_lora_dir):
            if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
                return {'error': 'Clothing LoRA directory not found.'}, 400
            flash('Clothing LoRA directory not found.', 'error')
            return redirect(url_for('outfits_index'))

        overwrite = request.form.get('overwrite') == 'true'

        system_prompt = load_prompt('outfit_system.txt')
        if not system_prompt:
            if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
                return {'error': 'Outfit system prompt file not found.'}, 500
            flash('Outfit system prompt file not found.', 'error')
            return redirect(url_for('outfits_index'))

        job_ids = []
        skipped = 0

        for filename in sorted(os.listdir(clothing_lora_dir)):
            if not filename.endswith('.safetensors'):
                continue

            name_base = filename.rsplit('.', 1)[0]
            outfit_id = re.sub(r'[^a-zA-Z0-9_]', '_', name_base.lower())
            outfit_name = re.sub(r'[^a-zA-Z0-9]+', ' ', name_base).title()

            json_filename = f"{outfit_id}.json"
            json_path = os.path.join(app.config['CLOTHING_DIR'], json_filename)

            is_existing = os.path.exists(json_path)
            if is_existing and not overwrite:
                skipped += 1
                continue

            # Read HTML companion file if it exists
            html_path = os.path.join(clothing_lora_dir, f"{name_base}.html")
            html_content = ""
            if os.path.exists(html_path):
                try:
                    with open(html_path, 'r', encoding='utf-8', errors='ignore') as hf:
                        html_raw = hf.read()
                        html_content = clean_html_text(html_raw)
                except Exception:
                    pass

            def make_task(fn, oid, oname, jp, lsf, html_ctx, sys_prompt, is_exist):
                def task_fn(job):
                    prompt = f"Create an outfit profile for a clothing LoRA based on the filename: '{fn}'"
                    if html_ctx:
                        prompt += f"\n\nHere is descriptive text extracted from an associated HTML file:\n###\n{html_ctx[:3000]}\n###"

                    llm_response = call_llm(prompt, sys_prompt)
                    clean_json = llm_response.replace('```json', '').replace('```', '').strip()
                    outfit_data = json.loads(clean_json)

                    outfit_data['outfit_id'] = oid
                    outfit_data['outfit_name'] = oname

                    if 'lora' not in outfit_data:
                        outfit_data['lora'] = {}
                    outfit_data['lora']['lora_name'] = f"Illustrious/{lsf}/{fn}"
                    if not outfit_data['lora'].get('lora_triggers'):
                        outfit_data['lora']['lora_triggers'] = fn.rsplit('.', 1)[0]
                    if outfit_data['lora'].get('lora_weight') is None:
                        outfit_data['lora']['lora_weight'] = 0.8
                    if outfit_data['lora'].get('lora_weight_min') is None:
                        outfit_data['lora']['lora_weight_min'] = 0.7
                    if outfit_data['lora'].get('lora_weight_max') is None:
                        outfit_data['lora']['lora_weight_max'] = 1.0

                    os.makedirs(os.path.dirname(jp), exist_ok=True)
                    with open(jp, 'w') as f:
                        json.dump(outfit_data, f, indent=2)

                    job['result'] = {'name': oname, 'action': 'overwritten' if is_exist else 'created'}
                return task_fn

            job = _enqueue_task(
                f"Create outfit: {outfit_name}",
                make_task(filename, outfit_id, outfit_name, json_path,
                         _lora_subfolder, html_content, system_prompt, is_existing)
            )
            job_ids.append(job['id'])

        # Enqueue a sync task to run after all creates
        if job_ids:
            def sync_task(job):
                sync_outfits()
                job['result'] = {'synced': True}
            _enqueue_task("Sync outfits DB", sync_task)

        if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
            return {'success': True, 'queued': len(job_ids), 'skipped': skipped}

        flash(f'Queued {len(job_ids)} outfit creation tasks ({skipped} skipped). Watch progress in the queue.')
        return redirect(url_for('outfits_index'))

    def _get_linked_characters_for_outfit(outfit):
        """Get all characters that have this outfit assigned."""
        linked = []
        all_chars = Character.query.all()
        for char in all_chars:
            if char.assigned_outfit_ids and outfit.outfit_id in char.assigned_outfit_ids:
                linked.append(char)
        return linked


    @app.route('/outfit/<path:slug>')
    def outfit_detail(slug):
        outfit = Outfit.query.filter_by(slug=slug).first_or_404()
        characters = Character.query.order_by(Character.name).all()

        # Load state from session
        preferences = session.get(f'prefs_outfit_{slug}')
        preview_image = session.get(f'preview_outfit_{slug}')
        selected_character = session.get(f'char_outfit_{slug}')
        extra_positive = session.get(f'extra_pos_outfit_{slug}', '')
        extra_negative = session.get(f'extra_neg_outfit_{slug}', '')

        # List existing preview images
        upload_dir = os.path.join(app.config['UPLOAD_FOLDER'], f"outfits/{slug}")
        existing_previews = []
        if os.path.isdir(upload_dir):
            files = sorted([f for f in os.listdir(upload_dir) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp'))], reverse=True)
            existing_previews = [f"outfits/{slug}/{f}" for f in files]

        # Get linked characters
        linked_characters = _get_linked_characters_for_outfit(outfit)

        return render_template('outfits/detail.html', outfit=outfit, characters=characters,
                              preferences=preferences, preview_image=preview_image,
                              selected_character=selected_character, existing_previews=existing_previews,
                              linked_characters=linked_characters,
                              extra_positive=extra_positive, extra_negative=extra_negative)

    @app.route('/outfit/<path:slug>/edit', methods=['GET', 'POST'])
    def edit_outfit(slug):
        outfit = Outfit.query.filter_by(slug=slug).first_or_404()
        loras = get_available_loras('outfits')  # Use clothing LoRAs for outfits

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

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

                # Update outfit_id if provided
                new_outfit_id = request.form.get('outfit_id', outfit.outfit_id)
                new_data['outfit_id'] = new_outfit_id

                # Update wardrobe section
                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 lora section
                if 'lora' in new_data:
                    for key in new_data['lora'].keys():
                        form_key = f"lora_{key}"
                        if form_key in request.form:
                            val = request.form.get(form_key)
                            if key == 'lora_weight':
                                try: val = float(val)
                                except: val = 0.8
                            new_data['lora'][key] = val

                # LoRA weight randomization bounds
                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)

                # Update Tags (structured dict)
                new_data['tags'] = {
                    'outfit_type': request.form.get('tag_outfit_type', '').strip(),
                    'nsfw': 'tag_nsfw' in request.form,
                }
                outfit.is_nsfw = new_data['tags']['nsfw']

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

                # 3. Write back to JSON file
                outfit_file = outfit.filename or f"{re.sub(r'[^a-zA-Z0-9_]', '', outfit.outfit_id)}.json"
                file_path = os.path.join(app.config['CLOTHING_DIR'], outfit_file)

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

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

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

        return render_template('outfits/edit.html', outfit=outfit, loras=loras)

    @app.route('/outfit/<path:slug>/generate', methods=['POST'])
    def generate_outfit_image(slug):
        outfit = Outfit.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 selected character (if any)
            character_slug = request.form.get('character_slug', '')
            character = None

            character = _resolve_character(character_slug)
            if character_slug == '__random__' and character:
                character_slug = character.slug

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

            # Save preferences
            session[f'prefs_outfit_{slug}'] = selected_fields
            session[f'char_outfit_{slug}'] = character_slug
            session[f'extra_pos_outfit_{slug}'] = extra_positive
            session[f'extra_neg_outfit_{slug}'] = extra_negative
            session.modified = True

            # Build combined data for prompt building
            if character:
                # Combine character identity/defaults with outfit wardrobe
                combined_data = {
                    'character_id': character.character_id,
                    'identity': character.data.get('identity', {}),
                    'defaults': character.data.get('defaults', {}),
                    'wardrobe': outfit.data.get('wardrobe', {}),  # Use outfit's wardrobe
                    'styles': character.data.get('styles', {}),  # Use character's styles
                    'lora': outfit.data.get('lora', {}),  # Use outfit's lora
                    'tags': outfit.data.get('tags', [])
                }

                # Merge character identity/defaults into selected_fields so they appear in the prompt
                if selected_fields:
                    _ensure_character_fields(character, selected_fields,
                                             include_wardrobe=False, include_defaults=True)
                else:
                    # No explicit field selection (e.g. batch generation) — build a selection
                    # that includes identity + wardrobe + name + lora triggers, but NOT character
                    # defaults (expression, pose, scene), so outfit covers stay generic.
                    from utils import _BODY_GROUP_KEYS
                    for key in _BODY_GROUP_KEYS:
                        if character.data.get('identity', {}).get(key):
                            selected_fields.append(f'identity::{key}')
                    outfit_wardrobe = outfit.data.get('wardrobe', {})
                    for key in _BODY_GROUP_KEYS:
                        if outfit_wardrobe.get(key):
                            selected_fields.append(f'wardrobe::{key}')
                    selected_fields.append('special::name')
                    if outfit.data.get('lora', {}).get('lora_triggers'):
                        selected_fields.append('lora::lora_triggers')

                default_fields = character.default_fields
            else:
                # Outfit only - no character
                combined_data = {
                    'character_id': outfit.outfit_id,
                    'wardrobe': outfit.data.get('wardrobe', {}),
                    'lora': outfit.data.get('lora', {}),
                    'tags': outfit.data.get('tags', [])
                }
                default_fields = outfit.default_fields

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

            # Queue generation
            with open('comfy_workflow.json', 'r') as f:
                workflow = json.load(f)

            # Build prompts for combined data
            prompts = build_prompt(combined_data, selected_fields, default_fields)

            _append_background(prompts, character)

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

            # Prepare workflow - pass both character and outfit for dual LoRA support
            ckpt_path, ckpt_data = _get_default_checkpoint()
            workflow = _prepare_workflow(workflow, character, prompts, outfit=outfit, custom_negative=extra_negative or None, checkpoint=ckpt_path, checkpoint_data=ckpt_data, fixed_seed=fixed_seed)

            char_label = character.name if character else 'no character'
            label = f"Outfit: {outfit.name} ({char_label}) – {action}"
            job = _enqueue_job(label, workflow, _make_finalize('outfits', slug, Outfit, action))

            if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
                return {'status': 'queued', 'job_id': job['id']}

            return redirect(url_for('outfit_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('outfit_detail', slug=slug))

    @app.route('/outfit/create', methods=['GET', 'POST'])
    def create_outfit():
        form_data = {}

        if request.method == 'POST':
            name = request.form.get('name')
            slug = request.form.get('filename', '').strip()
            prompt = request.form.get('prompt', '')
            use_llm = request.form.get('use_llm') == 'on'

            form_data = {'name': name, 'filename': slug, 'prompt': prompt, 'use_llm': use_llm}

            # 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 = 'outfit'

            # Find available filename (increment if exists)
            base_slug = safe_slug
            counter = 1
            while os.path.exists(os.path.join(app.config['CLOTHING_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:
                    flash("Description is required when AI generation is enabled.")
                    return render_template('outfits/create.html', form_data=form_data)

                # Generate JSON with LLM
                system_prompt = load_prompt('outfit_system.txt')
                if not system_prompt:
                    flash("System prompt file not found.")
                    return render_template('outfits/create.html', form_data=form_data)

                try:
                    llm_response = call_llm(f"Create an outfit profile for '{name}' based on this description: {prompt}", system_prompt)

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

                    # Enforce IDs
                    outfit_data['outfit_id'] = safe_slug
                    outfit_data['outfit_name'] = name

                    # Ensure required fields exist
                    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'] = []

                except Exception as e:
                    logger.exception("LLM error: %s", e)
                    flash(f"Failed to generate outfit profile: {e}")
                    return render_template('outfits/create.html', form_data=form_data)
            else:
                # Create blank outfit template
                outfit_data = {
                    "outfit_id": safe_slug,
                    "outfit_name": name,
                    "wardrobe": {
                        "base": "",
                        "head": "",
                        "upper_body": "",
                        "lower_body": "",
                        "hands": "",
                        "feet": "",
                        "additional": ""
                    },
                    "lora": {
                        "lora_name": "",
                        "lora_weight": 0.8,
                        "lora_triggers": ""
                    },
                    "tags": []
                }

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

                # Add to DB
                new_outfit = Outfit(
                    outfit_id=safe_slug,
                    slug=safe_slug,
                    filename=f"{safe_slug}.json",
                    name=name,
                    data=outfit_data
                )
                db.session.add(new_outfit)
                db.session.commit()

                flash('Outfit created successfully!')
                return redirect(url_for('outfit_detail', slug=safe_slug))

            except Exception as e:
                logger.exception("Save error: %s", e)
                flash(f"Failed to create outfit: {e}")
                return render_template('outfits/create.html', form_data=form_data)

        return render_template('outfits/create.html', form_data=form_data)


