import re
from models import db, Character
from utils import _IDENTITY_KEYS, _WARDROBE_KEYS, _BODY_GROUP_KEYS, parse_orientation


def _dedup_tags(prompt_str):
    """Remove duplicate tags from a comma-separated prompt string, preserving first-occurrence order."""
    seen = set()
    result = []
    for tag in prompt_str.split(','):
        t = tag.strip()
        if t and t.lower() not in seen:
            seen.add(t.lower())
            result.append(t)
    return ', '.join(result)


def _cross_dedup_prompts(positive, negative):
    """Remove tags shared between positive and negative prompts.

    Repeatedly strips the first occurrence from each side until the tag exists
    on only one side. Equal counts cancel out completely; any excess on one side
    retains the remainder, allowing deliberate overrides (e.g. adding a tag twice
    in the positive while it appears once in the negative leaves one copy positive).
    """
    def parse_tags(s):
        return [t.strip() for t in s.split(',') if t.strip()]

    pos_tags = parse_tags(positive)
    neg_tags = parse_tags(negative)

    shared = {t.lower() for t in pos_tags} & {t.lower() for t in neg_tags}
    for tag_lower in shared:
        while (
            any(t.lower() == tag_lower for t in pos_tags) and
            any(t.lower() == tag_lower for t in neg_tags)
        ):
            pos_tags.pop(next(i for i, t in enumerate(pos_tags) if t.lower() == tag_lower))
            neg_tags.pop(next(i for i, t in enumerate(neg_tags) if t.lower() == tag_lower))

    return ', '.join(pos_tags), ', '.join(neg_tags)


def _resolve_character(character_slug):
    """Resolve a character_slug string (possibly '__random__') to a Character instance."""
    if character_slug == '__random__':
        return Character.query.order_by(db.func.random()).first()
    if character_slug:
        return Character.query.filter_by(slug=character_slug).first()
    return None


def _ensure_character_fields(character, selected_fields, include_wardrobe=True, include_defaults=False):
    """Mutate selected_fields in place to include essential character identity/wardrobe/name keys.

    include_wardrobe  — also inject active wardrobe keys (default True)
    include_defaults  — also inject defaults::expression and defaults::pose (for outfit/look previews)
    """
    identity = character.data.get('identity', {})
    for key in _IDENTITY_KEYS:
        if identity.get(key):
            field_key = f'identity::{key}'
            if field_key not in selected_fields:
                selected_fields.append(field_key)
    if include_defaults:
        for key in ['expression', 'pose']:
            if character.data.get('defaults', {}).get(key):
                field_key = f'defaults::{key}'
                if field_key not in selected_fields:
                    selected_fields.append(field_key)
    if 'special::name' not in selected_fields:
        selected_fields.append('special::name')
    if include_wardrobe:
        wardrobe = character.get_active_wardrobe()
        for key in _WARDROBE_KEYS:
            if wardrobe.get(key):
                field_key = f'wardrobe::{key}'
                if field_key not in selected_fields:
                    selected_fields.append(field_key)


def _append_background(prompts, character=None):
    """Append a (color-prefixed) simple background tag to prompts['main']."""
    primary_color = character.data.get('styles', {}).get('primary_color', '') if character else ''
    bg = f"{primary_color} simple background" if primary_color else "simple background"
    prompts['main'] = f"{prompts['main']}, {bg}"


def build_prompt(data, selected_fields=None, default_fields=None, active_outfit='default'):
    def is_selected(section, key):
        # Priority:
        # 1. Manual selection from form (if list is not empty)
        # 2. Default fields (saved per character)
        # 3. Select all (fallback)
        if selected_fields is not None and len(selected_fields) > 0:
            return f"{section}::{key}" in selected_fields
        if default_fields:
            return f"{section}::{key}" in default_fields
        return True

    identity = data.get('identity', {})

    # Get wardrobe - handle both new nested format and legacy flat format
    wardrobe_data = data.get('wardrobe', {})
    if 'default' in wardrobe_data and isinstance(wardrobe_data.get('default'), dict):
        # New nested format - get active outfit
        wardrobe = wardrobe_data.get(active_outfit or 'default', wardrobe_data.get('default', {}))
    else:
        # Legacy flat format
        wardrobe = wardrobe_data

    defaults = data.get('defaults', {})
    action_data = data.get('action', {})
    style_data = data.get('style', {})
    participants = data.get('participants', {})

    # Helper: collect selected values from identity + wardrobe for a given group key
    def _group_val(key):
        parts = []
        id_val = identity.get(key, '')
        wd_val = wardrobe.get(key, '')
        if id_val and is_selected('identity', key):
            val = id_val
            # Filter out conflicting tags from base if participants data is present
            if participants and key == 'base':
                val = re.sub(r'\b(1girl|1boy|solo)\b', '', val).replace(', ,', ',').strip(', ')
            if val:
                parts.append(val)
        if wd_val and is_selected('wardrobe', key):
            parts.append(wd_val)
        return ', '.join(parts)

    # 1. Main Prompt
    parts = []

    # Handle participants logic
    if participants:
        if participants.get('solo_focus') == 'true':
            parts.append('(solo focus:1.2)')
        orientation = participants.get('orientation', '')
        if orientation:
            parts.extend(parse_orientation(orientation))
    else:
        parts.append("(solo:1.2)")

    # Use character_id (underscores to spaces) for tags compatibility
    char_tag = data.get('character_id', '').replace('_', ' ')
    if char_tag and is_selected('special', 'name'):
        parts.append(char_tag)

    # Add all body groups to main prompt
    for key in _BODY_GROUP_KEYS:
        val = _group_val(key)
        if val:
            parts.append(val)

    # Add defaults (expression, pose, scene)
    for key in ['expression', 'pose', 'scene']:
        val = defaults.get(key)
        if val and is_selected('defaults', key):
            parts.append(val)

    # Standard character styles
    char_aesthetic = data.get('styles', {}).get('aesthetic')
    if char_aesthetic and is_selected('styles', 'aesthetic'):
        parts.append(f"{char_aesthetic} style")

    # Styles Gallery logic
    if style_data.get('artist_name') and is_selected('style', 'artist_name'):
        parts.append(f"by {style_data['artist_name']}")
    if style_data.get('artistic_style') and is_selected('style', 'artistic_style'):
        parts.append(style_data['artistic_style'])

    lora = data.get('lora', {})
    if lora.get('lora_triggers') and is_selected('lora', 'lora_triggers'):
        parts.append(lora.get('lora_triggers'))

    # 2. Face Prompt: head group + expression + action head
    face_parts = []
    if char_tag and is_selected('special', 'name'):
        face_parts.append(char_tag)
    head_val = _group_val('head')
    if head_val:
        face_parts.append(head_val)
    if defaults.get('expression') and is_selected('defaults', 'expression'):
        face_parts.append(defaults.get('expression'))
    if action_data.get('head') and is_selected('action', 'head'):
        face_parts.append(action_data.get('head'))

    # 3. Hand Prompt: hands group + action hands
    hand_parts = []
    hands_val = _group_val('hands')
    if hands_val:
        hand_parts.append(hands_val)
    if action_data.get('hands') and is_selected('action', 'hands'):
        hand_parts.append(action_data.get('hands'))

    # 4. Feet Prompt: feet group + action feet
    feet_parts = []
    feet_val = _group_val('feet')
    if feet_val:
        feet_parts.append(feet_val)
    if action_data.get('feet') and is_selected('action', 'feet'):
        feet_parts.append(action_data.get('feet'))

    return {
        "main": _dedup_tags(", ".join(parts)),
        "face": _dedup_tags(", ".join(face_parts)),
        "hand": _dedup_tags(", ".join(hand_parts)),
        "feet": _dedup_tags(", ".join(feet_parts)),
    }


def build_multi_prompt(char_a, char_b, extras_prompt=''):
    """Build prompts for a two-character generation using BREAK separation.

    Returns dict with combined prompts (main, face, hand) and per-character
    prompts (char_a_main, char_a_face, char_b_main, char_b_face) for the
    per-person/face ADetailer passes.
    """
    # Build individual prompts with all fields selected
    prompts_a = build_prompt(char_a.data)
    prompts_b = build_prompt(char_b.data)

    # Strip solo/orientation tags from individual prompts — we'll add combined ones
    _solo_orientation_tags = {
        'solo', '(solo:1.2)', '(solo focus:1.2)',
        '1girl', '1boy', '2girls', '2boys', '3girls', '3boys',
        'hetero', 'yuri', 'yaoi',
        'multiple girls', 'multiple boys',
    }

    def _strip_tags(prompt_str, tags_to_remove):
        parts = [t.strip() for t in prompt_str.split(',') if t.strip()]
        return ', '.join(p for p in parts if p.lower() not in tags_to_remove)

    main_a = _strip_tags(prompts_a['main'], _solo_orientation_tags)
    main_b = _strip_tags(prompts_b['main'], _solo_orientation_tags)

    # Compute combined orientation
    orient_a = char_a.data.get('participants', {}).get('orientation', '1F')
    orient_b = char_b.data.get('participants', {}).get('orientation', '1F')

    # Count total M and F across both characters
    combined_m = orient_a.upper().count('M') + orient_b.upper().count('M')
    combined_f = orient_a.upper().count('F') + orient_b.upper().count('F')
    combined_orientation = f"{combined_m}M{combined_f}F" if combined_m else f"{combined_f}F"
    if combined_f == 0:
        combined_orientation = f"{combined_m}M"
    orientation_tags = parse_orientation(combined_orientation)

    # Build combined main prompt with BREAK separation
    orientation_str = ', '.join(orientation_tags)
    combined_main = f"{orientation_str}, {main_a} BREAK {main_b}"
    if extras_prompt:
        combined_main = f"{extras_prompt}, {combined_main}"

    # Merge face/hand prompts for the hand detailer (shared, not per-character)
    hand_parts = [p for p in [prompts_a['hand'], prompts_b['hand']] if p]

    return {
        "main": _dedup_tags(combined_main),
        "face": "",  # not used — per-character face prompts go to SEGS detailers
        "hand": _dedup_tags(', '.join(hand_parts)),
        # Per-character prompts for SEGS-based ADetailer passes
        "char_a_main": _dedup_tags(main_a),
        "char_a_face": _dedup_tags(prompts_a['face']),
        "char_b_main": _dedup_tags(main_b),
        "char_b_face": _dedup_tags(prompts_b['face']),
    }


def build_extras_prompt(actions, outfits, scenes, styles, detailers):
    """Combine positive prompt text from all selected category items."""
    parts = []

    for action in actions:
        data = action.data
        lora = data.get('lora', {})
        if lora.get('lora_triggers'):
            parts.append(lora['lora_triggers'])
        for key in _BODY_GROUP_KEYS:
            val = data.get('action', {}).get(key)
            if val:
                parts.append(val)

    for outfit in outfits:
        data = outfit.data
        wardrobe = data.get('wardrobe', {})
        for key in _BODY_GROUP_KEYS:
            val = wardrobe.get(key)
            if val:
                parts.append(val)
        lora = data.get('lora', {})
        if lora.get('lora_triggers'):
            parts.append(lora['lora_triggers'])

    for scene in scenes:
        data = scene.data
        scene_fields = data.get('scene', {})
        for key in ['background', 'foreground', 'lighting']:
            val = scene_fields.get(key)
            if val:
                parts.append(val)
        lora = data.get('lora', {})
        if lora.get('lora_triggers'):
            parts.append(lora['lora_triggers'])

    for style in styles:
        data = style.data
        style_fields = data.get('style', {})
        if style_fields.get('artist_name'):
            parts.append(f"by {style_fields['artist_name']}")
        if style_fields.get('artistic_style'):
            parts.append(style_fields['artistic_style'])
        lora = data.get('lora', {})
        if lora.get('lora_triggers'):
            parts.append(lora['lora_triggers'])

    for detailer in detailers:
        data = detailer.data
        prompt = data.get('prompt', '')
        if isinstance(prompt, list):
            parts.extend(p for p in prompt if p)
        elif prompt:
            parts.append(prompt)
        lora = data.get('lora', {})
        if lora.get('lora_triggers'):
            parts.append(lora['lora_triggers'])

    return ", ".join(p for p in parts if p)
