"""
FASE 2: Bucle principal 1G1R ejecucion LOCAL en NAS (Direct Disk I/O)
Cero sobrecarga de red SSH/rsync. Mismo sistema de candados atomicos.
"""
import json
import os
import shutil
import zipfile
import subprocess
import logging
import re
import html
import time
from collections import defaultdict

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler(os.path.expanduser("~/phase2_nas_local.log")),
        logging.StreamHandler()
    ]
)

BASE_DIR     = "/mnt/18tb/downloads/roms"
WINNERS_JSON = os.path.join(BASE_DIR, "_master_index_1g1r_optimizado.json")
if not os.path.exists(WINNERS_JSON):
    WINNERS_JSON = os.path.expanduser("~/1g1r_processor/_master_index_1g1r_optimizado.json")

NAS_DEST     = "/mnt/18tb/roms/1g1r"

def determine_local_tmp():
    for candidate in ['/tmp/1g1r_tmp', '/mnt/18tb/1g1r_tmp']:
        try:
            os.makedirs(candidate, exist_ok=True)
            test_file = os.path.join(candidate, '.write_test')
            with open(test_file, 'w') as f:
                f.write('1')
            os.remove(test_file)
            return candidate
        except Exception:
            continue
    return '/tmp/1g1r_tmp'

LOCAL_TMP = determine_local_tmp()

STORE_ONLY = {'.chd', '.rvz', '.wia', '.cso', '.zso', '.iso', '.pbp'}

HACK_SYSTEM_MAP = {
    'snes hacks':               'Nintendo - Super Nintendo Entertainment System',
    'nes hacks':                'Nintendo - Nintendo Entertainment System',
    'mega drive hacks':         'Sega - Mega Drive - Genesis',
    'master system hacks':      'Sega - Master System - Mark III',
    'game boy hacks':           'Nintendo - Game Boy',
    'game boy color hacks':     'Nintendo - Game Boy Color',
    'game boy advance hacks':   'Nintendo - Game Boy Advance',
    'n64 hacks':                'Nintendo - Nintendo 64',
    'playstation hacks':        'Sony - PlayStation',
    'playstation 2 hacks':      'Sony - PlayStation 2',
    'saturn hacks':             'Sega - Saturn',
    'pc engine hacks':          'NEC - PC Engine - TurboGrafx-16',
    '32x hacks':                'Sega - 32X',
    'game gear hacks':          'Sega - Game Gear',
    'neo geo pocket hacks':     'SNK - Neo Geo Pocket Color',
    'wonderswan hacks':         'Bandai - WonderSwan',
    'wonderswan color hacks':   'Bandai - WonderSwan Color',
    'msx2 hacks':               'Microsoft - MSX2',
    'nintendo ds hacks':        'Nintendo - Nintendo DS',
    'playstation portable hacks': 'Sony - PlayStation Portable',
}

def sanitize_dir(name):
    for c in ['/', '\\', ':', '*', '?', '"', '<', '>', '|']:
        name = name.replace(c, '-')
    return name.strip(' .-')

def get_canonical_system_and_sub(sys_name):
    clean = html.unescape(sys_name).strip()
    clean = clean.replace('(Parent-Clone)', '').strip()
    clean_lower = clean.lower()

    is_hack = False
    if clean_lower in HACK_SYSTEM_MAP:
        return (HACK_SYSTEM_MAP[clean_lower], True)
    if 'hacks' in clean_lower:
        is_hack = True
        clean = re.sub(r'\s*hacks\s*$', '', clean, flags=re.IGNORECASE).strip()
        clean_lower = clean.lower()

    # SEGA
    if 'mega drive' in clean_lower or 'genesis' in clean_lower:
        base = 'Sega - Mega Drive - Genesis'
    elif 'master system' in clean_lower or 'mark iii' in clean_lower:
        base = 'Sega - Master System - Mark III'
    elif 'game gear' in clean_lower:
        base = 'Sega - Game Gear'
    elif 'mega cd' in clean_lower or 'sega cd' in clean_lower:
        base = 'Sega - Mega CD & Sega CD'
    elif '32x' in clean_lower:
        base = 'Sega - 32X'
    elif 'sg-1000' in clean_lower or 'sc-3000' in clean_lower:
        base = 'Sega - SG-1000 - SC-3000'
    elif 'saturn' in clean_lower:
        base = 'Sega - Saturn'
    elif 'dreamcast' in clean_lower:
        base = 'Sega - Dreamcast'

    # NINTENDO
    elif '3ds' in clean_lower:
        base = 'Nintendo - Nintendo 3DS'
    elif 'nintendo 64' in clean_lower or 'n64' in clean_lower:
        base = 'Nintendo - Nintendo 64'
    elif 'super nintendo' in clean_lower or 'snes' in clean_lower:
        base = 'Nintendo - Super Nintendo Entertainment System'
    elif 'nintendo entertainment system' in clean_lower or re.search(r'\bnes\b', clean_lower):
        base = 'Nintendo - Nintendo Entertainment System'
    elif 'dsi' in clean_lower:
        base = 'Nintendo - Nintendo DSi'
    elif 'game boy advance' in clean_lower:
        if '(multiboot)' in clean_lower:
            base = 'Nintendo - Game Boy Advance (Multiboot)'
        elif '(video)' in clean_lower:
            base = 'Nintendo - Game Boy Advance (Video)'
        else:
            base = 'Nintendo - Game Boy Advance'
    elif 'game boy color' in clean_lower:
        base = 'Nintendo - Game Boy Color'
    elif 'game boy' in clean_lower and 'advance' not in clean_lower and 'color' not in clean_lower:
        base = 'Nintendo - Game Boy'
    elif 'gamecube' in clean_lower:
        base = 'Nintendo - GameCube'
    elif 'virtual boy' in clean_lower:
        base = 'Nintendo - Virtual Boy'
    elif 'family computer disk system' in clean_lower or 'fds' in clean_lower:
        base = 'Nintendo - Family Computer Disk System (FDS)'
    elif 'satellaview' in clean_lower:
        base = 'Nintendo - Satellaview'
    elif 'sufami turbo' in clean_lower:
        base = 'Nintendo - Sufami Turbo'

    # SONY
    elif 'playstation portable' in clean_lower or 'psp' in clean_lower:
        base = 'Sony - PlayStation Portable'
    elif 'playstation 2' in clean_lower or 'ps2' in clean_lower:
        base = 'Sony - PlayStation 2'
    elif 'playstation' in clean_lower and 'portable' not in clean_lower and 'vita' not in clean_lower:
        base = 'Sony - PlayStation'

    # NEC
    elif 'pc engine cd' in clean_lower or 'turbografx cd' in clean_lower:
        base = 'NEC - PC Engine CD & TurboGrafx CD'
    elif 'pc engine supergrafx' in clean_lower:
        base = 'NEC - PC Engine SuperGrafx'
    elif 'pc engine' in clean_lower or 'turbografx' in clean_lower:
        base = 'NEC - PC Engine - TurboGrafx-16'

    # SNK
    elif 'neogeo pocket color' in clean_lower or 'neo geo pocket color' in clean_lower:
        base = 'SNK - Neo Geo Pocket Color'
    elif 'neogeo pocket' in clean_lower or 'neo geo pocket' in clean_lower:
        base = 'SNK - NeoGeo Pocket'
    elif 'neo geo cd' in clean_lower:
        base = 'SNK - Neo Geo CD'

    # ATARI
    elif 'atari 7800' in clean_lower:
        base = 'Atari - Atari 7800'
    elif 'atari jaguar' in clean_lower:
        base = 'Atari - Atari Jaguar'
    elif 'atari lynx' in clean_lower:
        base = 'Atari - Atari Lynx'

    # OTHERS
    elif 'commodore 64' in clean_lower:
        base = 'Commodore - Commodore 64'
    elif 'amstrad - cpc' in clean_lower:
        base = 'Amstrad - CPC'
    elif 'wonderswan color' in clean_lower:
        base = 'Bandai - WonderSwan Color'
    elif 'wonderswan' in clean_lower:
        base = 'Bandai - WonderSwan'
    elif not clean or 'sin catalogar' in clean_lower:
        base = '_sin_catalogar'
    else:
        clean = re.sub(r'\s*\((?:Decrypted|Encrypted|Headered|Headerless|ByteSwapped|BigEndian|A78|BIN|J64|JAG|LYX|BLL|Misc|Sector)\)\s*', ' ', clean).strip()
        base = clean

    return (sanitize_dir(base), is_hack)

def get_nas_dest_dir(system_name):
    base_dir, is_hack = get_canonical_system_and_sub(system_name)
    if is_hack:
        return f"{NAS_DEST}/{base_dir}/hacks"
    return f"{NAS_DEST}/{base_dir}"

def load_existing_nas_files():
    logging.info("Indexando archivos .7z ya existentes en NAS (Local)...")
    cmd = ['find', NAS_DEST, '-type', 'f', '-name', '*.7z']
    r = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
    existing = set()
    if r.returncode == 0:
        for line in r.stdout.splitlines():
            line = line.strip()
            if line:
                existing.add(line)
    logging.info(f"Archivos .7z ya presentes en NAS (indexados): {len(existing)}")
    return existing

def acquire_local_lock(nas_final_path):
    lock_path = nas_final_path + ".lock"
    os.makedirs(os.path.dirname(lock_path), exist_ok=True)
    if os.path.exists(nas_final_path) or os.path.exists(lock_path):
        return False
    try:
        with open(lock_path, 'x'):
            pass
        return True
    except Exception:
        return False

def release_local_lock(nas_final_path):
    lock_path = nas_final_path + ".lock"
    if os.path.exists(lock_path):
        try:
            os.remove(lock_path)
        except Exception:
            pass

def extract_file(container_path, internal_path, work_dir, container_type):
    basename = os.path.basename(internal_path)
    out_path = os.path.join(work_dir, basename)

    if container_type == 'zip' or container_path.lower().endswith('.zip'):
        try:
            with zipfile.ZipFile(container_path, 'r') as zf:
                matching = [n for n in zf.namelist()
                            if n == internal_path or n.endswith('/' + os.path.basename(internal_path))]
                if not matching:
                    return None
                data = zf.read(matching[0])
                with open(out_path, 'wb') as f:
                    f.write(data)
                return out_path
        except Exception as e:
            logging.error(f"    ZIP extract error: {e}")
            return None

    elif container_type in ('rar', '7z') or container_path.lower().endswith(('.rar', '.7z')):
        tool = 'unrar' if (container_type == 'rar' or container_path.lower().endswith('.rar')) else '7z'
        if tool == 'unrar':
            cmd = ['unrar', 'e', '-y', '-inul', '--', container_path, internal_path, work_dir]
        else:
            cmd = ['7z', 'e', '-y', f'-o{work_dir}', '--', container_path, internal_path]
        r = subprocess.run(cmd, capture_output=True, timeout=600)
        return out_path if os.path.exists(out_path) else None

    return None

def compress_to_7z(files_in_workdir, game_name, work_dir):
    sevenz_name = game_name + '.7z'
    sevenz_path = os.path.join(work_dir, sevenz_name)

    exts = {os.path.splitext(f)[1].lower() for f in files_in_workdir}
    level = '-mx=0' if exts & STORE_ONLY else '-mx=9'

    cmd = ['7z', 'a', level, '-mmt=4', '-ms=on', '--', sevenz_path] + files_in_workdir
    r = subprocess.run(cmd, capture_output=True, text=True, timeout=3600)
    if r.returncode != 0:
        logging.error(f"    7z create error: {r.stderr[:200]}")
        return None

    r2 = subprocess.run(['7z', 't', '--', sevenz_path], capture_output=True, text=True, timeout=600)
    if r2.returncode != 0:
        logging.error(f"    7z test FAIL: {sevenz_path}")
        if os.path.exists(sevenz_path):
            try:
                os.remove(sevenz_path)
            except Exception:
                pass
        return None

    return sevenz_path

def process_game(game_name, system_name, items, nas_dest_dir, existing_set):
    sevenz_filename = game_name + '.7z'
    nas_final_path = f"{nas_dest_dir}/{sevenz_filename}"

    if nas_final_path in existing_set:
        return 'skipped'

    if not acquire_local_lock(nas_final_path):
        return 'skipped'

    try:
        work_dir = os.path.join(LOCAL_TMP, re.sub(r'[^\w\-]', '_', game_name)[:60])
        os.makedirs(work_dir, exist_ok=True)

        extracted_files = []

        for item in items:
            ruta    = item.get('Ruta_Absoluta_Disco', '')
            interna = item.get('Ruta_Interna', '') or ''
            comprim = item.get('Es_Comprimido', False)
            cont    = item.get('Tipo_Contenedor', '') or ''
            nombre  = item.get('Nombre_Fichero', '')

            if not ruta or not os.path.exists(ruta):
                continue

            if comprim:
                extracted = extract_file(ruta, interna, work_dir, cont)
                if extracted:
                    extracted_files.append(extracted)
                else:
                    logging.warning(f"    No extraído: {interna} de {os.path.basename(ruta)}")
            else:
                local_path = os.path.join(work_dir, nombre)
                try:
                    shutil.copy2(ruta, local_path)
                    extracted_files.append(local_path)
                except Exception as e:
                    logging.error(f"    Copy error: {e}")
                    shutil.rmtree(work_dir, ignore_errors=True)
                    return 'error'

        if not extracted_files:
            logging.warning(f"    Sin ficheros extraídos para: {game_name}")
            shutil.rmtree(work_dir, ignore_errors=True)
            return 'error'

        sevenz_path = compress_to_7z(extracted_files, game_name, work_dir)
        if not sevenz_path:
            shutil.rmtree(work_dir, ignore_errors=True)
            return 'error'

        os.makedirs(nas_dest_dir, exist_ok=True)
        final_dest = os.path.join(nas_dest_dir, sevenz_filename)
        shutil.move(sevenz_path, final_dest)

        existing_set.add(nas_final_path)
        return 'ok'
    finally:
        shutil.rmtree(work_dir, ignore_errors=True)
        release_local_lock(nas_final_path)

def main():
    os.makedirs(LOCAL_TMP, exist_ok=True)
    logging.info(f"Directorio de trabajo local NAS: {LOCAL_TMP}")

    existing_set = load_existing_nas_files()

    logging.info("Cargando ganadores...")
    with open(WINNERS_JSON, 'r', encoding='utf-8') as f:
        all_items = json.load(f)

    ganadores = [x for x in all_items if x.get('1G1R_Motivo') == 'Ganador Oficial']
    logging.info(f"Total ganadores a procesar: {len(ganadores)}")

    grupos = defaultdict(list)
    for item in ganadores:
        sys_name  = item.get('1G1R_System', 'Desconocido')
        game_name = item.get('1G1R_Name', item.get('Nombre_Fichero', 'unknown'))
        grupos[(sys_name, game_name)].append(item)

    logging.info(f"Juegos únicos: {len(grupos)}")

    ok = errors = skipped = 0

    for idx, ((sys_name, game_name), items) in enumerate(sorted(grupos.items())):
        nas_dest_dir = get_nas_dest_dir(sys_name)

        result = process_game(game_name, sys_name, items, nas_dest_dir, existing_set)

        if result == 'ok':
            ok += 1
            logging.info(f"[{idx+1}/{len(grupos)}] OK: {sys_name} / {game_name}")
        elif result == 'skipped':
            skipped += 1
        else:
            errors += 1
            logging.warning(f"[{idx+1}/{len(grupos)}] ERROR: {sys_name} / {game_name}")

        if (ok + errors + skipped) % 100 == 0:
            logging.info(f"  --- Progreso: {ok} OK | {errors} ERR | {skipped} SKIP ---")

    logging.info("=" * 60)
    logging.info("RESUMEN FASE 2 - LOCAL NAS")
    logging.info(f"  Procesados OK:       {ok}")
    logging.info(f"  Ya existían (skip):  {skipped}")
    logging.info(f"  Errores:             {errors}")
    logging.info("=" * 60)

if __name__ == '__main__':
    main()
