import json
import os
import logging
import random
from flask import request, session, current_app
from models import db, Character, Look, Outfit, Action, Style, Scene, Detailer
from sqlalchemy.orm.attributes import flag_modified
from services.prompts import build_prompt, _dedup_tags, _cross_dedup_prompts
from services.workflow import _get_default_checkpoint, _apply_checkpoint_settings, _log_workflow_prompts
from services.job_queue import _enqueue_job
from services.comfyui import get_history, get_image

logger = logging.getLogger('gaze')

_STRENGTHS_MODEL_MAP = {
    'characters': Character,
    'looks': Look,
    'outfits': Outfit,
    'actions': Action,
    'styles': Style,
    'scenes': Scene,
    'detailers': Detailer,
}

_CATEGORY_LORA_NODES = {
    'characters': '16',
    'looks': '16',
    'outfits': '17',
    'actions': '18',
    'styles': '19',
    'scenes': '19',
    'detailers': '19',
}

_STRENGTHS_DATA_DIRS = {
    'characters': 'CHARACTERS_DIR',
    'looks':      'LOOKS_DIR',
    'outfits':    'CLOTHING_DIR',
    'actions':    'ACTIONS_DIR',
    'styles':     'STYLES_DIR',
    'scenes':     'SCENES_DIR',
    'detailers':  'DETAILERS_DIR',
}


def register_routes(app):

    def _get_character_data_without_lora(character):
        """Extract character data excluding LoRA to prevent activation in strengths gallery."""
        if not character:
            return None
        return {k: v for k, v in character.data.items() if k != 'lora'}

    def _build_strengths_prompts(category, entity, character, action=None, extra_positive=''):
        """Build main/face/hand prompt strings for the Strengths Gallery."""
        if category == 'characters':
            return build_prompt(entity.data, [], entity.default_fields)

        if category == 'looks':
            char_data_no_lora = _get_character_data_without_lora(character)
            base = build_prompt(char_data_no_lora, [], character.default_fields) if char_data_no_lora else {'main': '', 'face': '', 'hand': ''}
            look_pos = entity.data.get('positive', '')
            look_triggers = entity.data.get('lora', {}).get('lora_triggers', '')
            prefix_parts = [p for p in [look_triggers, look_pos] if p]
            prefix = ', '.join(prefix_parts)
            if prefix:
                base['main'] = f"{prefix}, {base['main']}" if base['main'] else prefix
            return base

        if category == 'outfits':
            wardrobe = entity.data.get('wardrobe', {})
            outfit_triggers = entity.data.get('lora', {}).get('lora_triggers', '')
            wardrobe_parts = [v for v in wardrobe.values() if isinstance(v, str) and v]
            char_parts = []
            face_parts = []
            hand_parts = []
            if character:
                identity = character.data.get('identity', {})
                defaults = character.data.get('defaults', {})
                char_parts = [v for v in [identity.get('base'), identity.get('head'),
                                           defaults.get('expression')] if v]
                face_parts = [v for v in [identity.get('head'),
                                           defaults.get('expression')] if v]
                hand_parts = [v for v in [wardrobe.get('hands')] if v]
            main_parts = ([outfit_triggers] if outfit_triggers else []) + char_parts + wardrobe_parts
            return {
                'main': _dedup_tags(', '.join(p for p in main_parts if p)),
                'face': _dedup_tags(', '.join(face_parts)),
                'hand': _dedup_tags(', '.join(hand_parts)),
            }

        if category == 'actions':
            action_data = entity.data.get('action', {})
            action_triggers = entity.data.get('lora', {}).get('lora_triggers', '')
            from utils import _BODY_GROUP_KEYS
            pose_parts = [action_data.get(k, '') for k in _BODY_GROUP_KEYS if action_data.get(k)]
            expr_parts = [action_data.get('head', '')] if action_data.get('head') else []
            char_parts = []
            face_parts = list(expr_parts)
            hand_parts = [action_data.get('hands', '')] if action_data.get('hands') else []
            if character:
                identity = character.data.get('identity', {})
                char_parts = [v for v in [identity.get('base'), identity.get('head')] if v]
                face_parts = [v for v in [identity.get('head')] + expr_parts if v]
            main_parts = ([action_triggers] if action_triggers else []) + char_parts + pose_parts
            return {
                'main': _dedup_tags(', '.join(p for p in main_parts if p)),
                'face': _dedup_tags(', '.join(face_parts)),
                'hand': _dedup_tags(', '.join(hand_parts)),
            }

        # styles / scenes / detailers
        entity_triggers = entity.data.get('lora', {}).get('lora_triggers', '')

        if category == 'styles':
            sdata = entity.data.get('style', {})
            artist = f"by {sdata['artist_name']}" if sdata.get('artist_name') else ''
            style_tags = sdata.get('artistic_style', '')
            entity_parts = [p for p in [entity_triggers, artist, style_tags] if p]
        elif category == 'scenes':
            sdata = entity.data.get('scene', {})
            scene_parts = [v for v in sdata.values() if isinstance(v, str) and v]
            entity_parts = [p for p in [entity_triggers] + scene_parts if p]
        else:  # detailers
            det_prompt = entity.data.get('prompt', '')
            entity_parts = [p for p in [entity_triggers, det_prompt] if p]

        char_data_no_lora = _get_character_data_without_lora(character)
        base = build_prompt(char_data_no_lora, [], character.default_fields) if char_data_no_lora else {'main': '', 'face': '', 'hand': '', 'feet': ''}
        entity_str = ', '.join(entity_parts)
        if entity_str:
            base['main'] = f"{base['main']}, {entity_str}" if base['main'] else entity_str

        if action is not None:
            action_data = action.data.get('action', {})
            from utils import _BODY_GROUP_KEYS
            action_parts = [action_data.get(k, '') for k in _BODY_GROUP_KEYS
                            if action_data.get(k)]
            action_str = ', '.join(action_parts)
            if action_str:
                base['main'] = f"{base['main']}, {action_str}" if base['main'] else action_str

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

        return base

    def _prepare_strengths_workflow(workflow, category, entity, character, prompts,
                                     checkpoint, ckpt_data, strength_value, fixed_seed,
                                     custom_negative=''):
        """Wire a ComfyUI workflow with ONLY the entity's LoRA active at a specific strength."""
        active_node = _CATEGORY_LORA_NODES.get(category, '16')
        entity_lora = entity.data.get('lora', {})
        entity_lora_name = entity_lora.get('lora_name', '')

        if checkpoint and '4' in workflow:
            workflow['4']['inputs']['ckpt_name'] = checkpoint

        if '5' in workflow:
            workflow['5']['inputs']['width'] = 1024
            workflow['5']['inputs']['height'] = 1024

        if '6' in workflow:
            workflow['6']['inputs']['text'] = workflow['6']['inputs']['text'].replace(
                '{{POSITIVE_PROMPT}}', prompts.get('main', ''))
        if '14' in workflow:
            workflow['14']['inputs']['text'] = workflow['14']['inputs']['text'].replace(
                '{{FACE_PROMPT}}', prompts.get('face', ''))
        if '15' in workflow:
            workflow['15']['inputs']['text'] = workflow['15']['inputs']['text'].replace(
                '{{HAND_PROMPT}}', prompts.get('hand', ''))

        if category == 'looks':
            look_neg = entity.data.get('negative', '')
            if look_neg and '7' in workflow:
                workflow['7']['inputs']['text'] = f"{look_neg}, {workflow['7']['inputs']['text']}"

        if custom_negative and '7' in workflow:
            workflow['7']['inputs']['text'] = f"{custom_negative}, {workflow['7']['inputs']['text']}"

        model_source = ['4', 0]
        clip_source = ['4', 1]

        for node_id in ['16', '17', '18', '19']:
            if node_id not in workflow:
                continue
            if node_id == active_node and entity_lora_name:
                workflow[node_id]['inputs']['lora_name'] = entity_lora_name
                workflow[node_id]['inputs']['strength_model'] = float(strength_value)
                workflow[node_id]['inputs']['strength_clip'] = float(strength_value)
                workflow[node_id]['inputs']['model'] = list(model_source)
                workflow[node_id]['inputs']['clip'] = list(clip_source)
                model_source = [node_id, 0]
                clip_source = [node_id, 1]

        for consumer, needs_model, needs_clip in [
            ('3',  True,  False),
            ('6',  False, True),
            ('7',  False, True),
            ('11', True,  True),
            ('13', True,  True),
            ('14', False, True),
            ('15', False, True),
        ]:
            if consumer in workflow:
                if needs_model:
                    workflow[consumer]['inputs']['model'] = list(model_source)
                if needs_clip:
                    workflow[consumer]['inputs']['clip'] = list(clip_source)

        for seed_node in ['3', '11', '13']:
            if seed_node in workflow:
                workflow[seed_node]['inputs']['seed'] = int(fixed_seed)

        if ckpt_data:
            workflow = _apply_checkpoint_settings(workflow, ckpt_data)

        sampler_name = workflow['3']['inputs'].get('sampler_name')
        scheduler = workflow['3']['inputs'].get('scheduler')
        for node_id in ['11', '13']:
            if node_id in workflow:
                if sampler_name:
                    workflow[node_id]['inputs']['sampler_name'] = sampler_name
                if scheduler:
                    workflow[node_id]['inputs']['scheduler'] = scheduler

        pos_text, neg_text = _cross_dedup_prompts(
            workflow['6']['inputs']['text'],
            workflow['7']['inputs']['text']
        )
        workflow['6']['inputs']['text'] = pos_text
        workflow['7']['inputs']['text'] = neg_text

        _log_workflow_prompts(f"_prepare_strengths_workflow [node={active_node} lora={entity_lora_name} @ {strength_value} seed={fixed_seed}]", workflow)
        return workflow

    @app.route('/strengths/<category>/<path:slug>/generate', methods=['POST'])
    def strengths_generate(category, slug):
        if category not in _STRENGTHS_MODEL_MAP:
            return {'error': 'unknown category'}, 400

        Model = _STRENGTHS_MODEL_MAP[category]
        entity = Model.query.filter_by(slug=slug).first_or_404()

        try:
            strength_value = float(request.form.get('strength_value', 1.0))
            fixed_seed = int(request.form.get('seed', random.randint(1, 10**15)))

            _singular = {
                'outfits': 'outfit', 'actions': 'action', 'styles': 'style',
                'scenes': 'scene', 'detailers': 'detailer', 'looks': 'look',
            }
            session_prefix = _singular.get(category, category)
            char_slug = (request.form.get('character_slug') or
                         session.get(f'char_{session_prefix}_{slug}'))

            if category == 'characters':
                character = entity
            elif char_slug == '__random__':
                character = Character.query.order_by(db.func.random()).first()
            elif char_slug:
                character = Character.query.filter_by(slug=char_slug).first()
            else:
                character = None

            logger.debug("Strengths: char_slug=%r -> character=%s", char_slug, character.slug if character else 'none')

            action_obj = None
            extra_positive = ''
            extra_negative = ''
            if category == 'detailers':
                action_slug = session.get(f'action_detailer_{slug}')
                if action_slug:
                    action_obj = Action.query.filter_by(slug=action_slug).first()
                extra_positive = session.get(f'extra_pos_detailer_{slug}', '')
                extra_negative = session.get(f'extra_neg_detailer_{slug}', '')
                logger.debug("Strengths: detailer session -- char=%s, action=%s, extra_pos=%s, extra_neg=%s", char_slug, action_slug, bool(extra_positive), bool(extra_negative))

            prompts = _build_strengths_prompts(category, entity, character,
                                               action=action_obj, extra_positive=extra_positive)

            checkpoint, ckpt_data = _get_default_checkpoint()
            workflow_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'comfy_workflow.json')
            with open(workflow_path, 'r') as f:
                workflow = json.load(f)

            workflow = _prepare_strengths_workflow(
                workflow, category, entity, character, prompts,
                checkpoint, ckpt_data, strength_value, fixed_seed,
                custom_negative=extra_negative
            )

            _category = category
            _slug = slug
            _strength_value = strength_value
            _fixed_seed = fixed_seed
            def _finalize(comfy_prompt_id, job):
                history = get_history(comfy_prompt_id)
                outputs = history[comfy_prompt_id].get('outputs', {})
                img_data = None
                for node_output in outputs.values():
                    for img in node_output.get('images', []):
                        img_data = get_image(img['filename'], img.get('subfolder', ''), img.get('type', 'output'))
                        break
                    if img_data:
                        break
                if not img_data:
                    raise Exception('no image in output')
                strength_str = f"{_strength_value:.2f}".replace('.', '_')
                upload_dir = os.path.join(current_app.config['UPLOAD_FOLDER'], _category, _slug, 'strengths')
                os.makedirs(upload_dir, exist_ok=True)
                out_filename = f"strength_{strength_str}_seed_{_fixed_seed}.png"
                out_path = os.path.join(upload_dir, out_filename)
                with open(out_path, 'wb') as f:
                    f.write(img_data)
                relative = f"{_category}/{_slug}/strengths/{out_filename}"
                job['result'] = {'image_url': f"/static/uploads/{relative}", 'strength_value': _strength_value}

            label = f"Strengths: {entity.name} @ {strength_value:.2f}"
            job = _enqueue_job(label, workflow, _finalize)
            return {'status': 'queued', 'job_id': job['id']}

        except Exception as e:
            logger.exception("Strengths generate error: %s", e)
            return {'error': str(e)}, 500

    @app.route('/strengths/<category>/<path:slug>/list')
    def strengths_list(category, slug):
        upload_dir = os.path.join(current_app.config['UPLOAD_FOLDER'], category, slug, 'strengths')
        if not os.path.isdir(upload_dir):
            return {'images': []}

        images = []
        for fname in sorted(os.listdir(upload_dir)):
            if not fname.endswith('.png'):
                continue
            try:
                parts = fname.replace('strength_', '').split('_seed_')
                strength_raw = parts[0]
                strength_display = strength_raw.replace('_', '.')
            except Exception:
                strength_display = fname
            images.append({
                'url': f"/static/uploads/{category}/{slug}/strengths/{fname}",
                'strength': strength_display,
                'filename': fname,
            })
        return {'images': images}

    @app.route('/strengths/<category>/<path:slug>/clear', methods=['POST'])
    def strengths_clear(category, slug):
        upload_dir = os.path.join(current_app.config['UPLOAD_FOLDER'], category, slug, 'strengths')
        if os.path.isdir(upload_dir):
            for fname in os.listdir(upload_dir):
                fpath = os.path.join(upload_dir, fname)
                if os.path.isfile(fpath):
                    os.remove(fpath)
        return {'success': True}

    @app.route('/strengths/<category>/<path:slug>/save_range', methods=['POST'])
    def strengths_save_range(category, slug):
        """Save lora_weight_min / lora_weight_max from the Strengths Gallery back to the entity JSON + DB."""
        if category not in _STRENGTHS_MODEL_MAP or category not in _STRENGTHS_DATA_DIRS:
            return {'error': 'unknown category'}, 400

        try:
            min_w = float(request.form.get('min_weight', ''))
            max_w = float(request.form.get('max_weight', ''))
        except (ValueError, TypeError):
            return {'error': 'invalid weight values'}, 400

        if min_w > max_w:
            min_w, max_w = max_w, min_w

        Model = _STRENGTHS_MODEL_MAP[category]
        entity = Model.query.filter_by(slug=slug).first_or_404()

        data = dict(entity.data)
        if 'lora' not in data or not isinstance(data.get('lora'), dict):
            return {'error': 'entity has no lora section'}, 400

        data['lora']['lora_weight_min'] = min_w
        data['lora']['lora_weight_max'] = max_w
        entity.data = data
        flag_modified(entity, 'data')

        data_dir = current_app.config[_STRENGTHS_DATA_DIRS[category]]
        filename = getattr(entity, 'filename', None) or f"{slug}.json"
        file_path = os.path.join(data_dir, filename)
        if os.path.exists(file_path):
            with open(file_path, 'w', encoding='utf-8') as f:
                json.dump(data, f, indent=2, ensure_ascii=False)
                f.write('\n')

        db.session.commit()
        return {'success': True, 'lora_weight_min': min_w, 'lora_weight_max': max_w}
