#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Extracción read-only de datos del sistema legacy SKEEN.
Uso autorizado por el responsable del proyecto con credenciales proporcionadas.
Solo lee datos; no modifica nada en https://sistema.skeenmx.app/
"""

import requests
import csv
import json
import re
import sys
import time
from pathlib import Path
from datetime import datetime, timedelta
from concurrent.futures import ThreadPoolExecutor, as_completed
from bs4 import BeautifulSoup

BASE = 'https://sistema.skeenmx.app'
USERNAME = 'JoseSkeen'
PASSWORD = 'SKr#902_88'
OUTDIR = Path('/root/migracion/extraccion')
OUTDIR.mkdir(parents=True, exist_ok=True)
CACHE = OUTDIR / 'detalle_pacientes.json'

session = requests.Session()


def log(msg):
    print(f'[{datetime.now().strftime("%H:%M:%S")}] {msg}')


def login():
    log('Iniciando sesión...')
    r = session.get(f'{BASE}/login', timeout=30)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, 'html.parser')
    token = soup.find('input', {'name': '_token'})
    token = token['value'] if token else ''
    data = {
        '_token': token,
        'username': USERNAME,
        'password': PASSWORD,
    }
    r = session.post(f'{BASE}/login', data=data, timeout=30, allow_redirects=True)
    r.raise_for_status()
    if '/login' in r.url or 'estas credenciales' in r.text.lower():
        raise Exception('Login fallido. Verifica credenciales.')
    log('Login exitoso')
    return True


def dt_params(start=0, length=100, search=''):
    """Genera parámetros DataTables server-side."""
    return {
        'draw': 1,
        'columns[0][data]': 'id',
        'columns[0][name]': '',
        'columns[0][searchable]': 'true',
        'columns[0][orderable]': 'true',
        'columns[0][search][value]': '',
        'columns[0][search][regex]': 'false',
        'start': start,
        'length': length,
        'search[value]': search,
        'search[regex]': 'false',
        'order[0][column]': '0',
        'order[0][dir]': 'asc',
        '_': int(time.time() * 1000),
    }


def fetch_json(url, params=None):
    headers = {'X-Requested-With': 'XMLHttpRequest'}
    r = session.get(f'{BASE}{url}', params=params or {}, headers=headers, timeout=120)
    r.raise_for_status()
    return r.json()


def paginate(url, params_builder, label, page_size=500, max_pages=None):
    """Recorre un endpoint DataTables paginado y devuelve todas las filas."""
    log(f'Extrayendo {label} desde {url}...')
    all_rows = []
    start = 0
    page = 0
    while True:
        params = params_builder(start, page_size)
        try:
            data = fetch_json(url, params)
        except Exception as e:
            log(f'  ERROR página {page}: {e}')
            break
        rows = data.get('data', [])
        if not rows:
            break
        all_rows.extend(rows)
        total = data.get('recordsTotal') or data.get('recordsFiltered') or 0
        log(f'  página {page + 1}: {len(rows)} filas (total acumulado: {len(all_rows)}/{total})')
        if len(rows) < page_size:
            break
        start += page_size
        page += 1
        if max_pages and page >= max_pages:
            break
        time.sleep(0.3)
    log(f'{label}: {len(all_rows)} registros extraídos')
    return all_rows


def save_csv(name, rows, fieldnames):
    path = OUTDIR / f'{name}.csv'
    with open(path, 'w', newline='', encoding='utf-8') as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames, extrasaction='ignore')
        writer.writeheader()
        writer.writerows(rows)
    log(f'Guardado {path}: {len(rows)} registros')


MESES_ES = {
    'enero': '01', 'febrero': '02', 'marzo': '03', 'abril': '04',
    'mayo': '05', 'junio': '06', 'julio': '07', 'agosto': '08',
    'septiembre': '09', 'octubre': '10', 'noviembre': '11', 'diciembre': '12',
}


def parse_spanish_date(text):
    """Convierte '05 de Septiembre del 1998' → '1998-09-05'."""
    text = (text or '').strip().lower()
    if not text:
        return ''
    m = re.search(r'(\d{1,2})\s+de\s+([a-záéíóúñ]+)\s+(?:del|de)\s+(\d{4})', text)
    if not m:
        return ''
    dia, mes, anio = m.groups()
    mes_num = MESES_ES.get(mes)
    if not mes_num:
        return ''
    return f'{anio}-{mes_num}-{int(dia):02d}'


def parse_patient_detail(html, fallback_name=''):
    """Extrae teléfono, email, fecha de nacimiento y sexo del detalle."""
    soup = BeautifulSoup(html, 'html.parser')
    data = {
        'celular': '',
        'whatsapp': '',
        'email': '',
        'fecha_nacimiento': '',
        'sexo': '',
    }
    # Campos de tablas
    for tr in soup.find_all('tr'):
        th = tr.find('th')
        td = tr.find('td')
        if not th or not td:
            continue
        key = ' '.join(th.get_text(strip=True).split()).lower().rstrip(':')
        val = ' '.join(td.get_text(strip=True).split())
        if key == 'celular':
            data['celular'] = re.sub(r'\D', '', val)
        elif key == 'whatsapp':
            data['whatsapp'] = re.sub(r'\D', '', val)
        elif key == 'sexo':
            data['sexo'] = 'female' if 'femenino' in val.lower() else ('male' if 'masculino' in val.lower() else val)
        elif key == 'fecha de nacimiento':
            data['fecha_nacimiento'] = parse_spanish_date(val)

    # Email suele aparecer como texto suelto o en un mailto
    emails = re.findall(r'[\w.\-]+@[\w.\-]+', soup.get_text())
    if emails:
        data['email'] = emails[0]

    return data


def load_detail_cache():
    if CACHE.exists():
        try:
            with open(CACHE, 'r', encoding='utf-8') as f:
                return json.load(f)
        except Exception as e:
            log(f'No se pudo cargar caché: {e}')
    return {}


def save_detail_cache(cache):
    with open(CACHE, 'w', encoding='utf-8') as f:
        json.dump(cache, f, ensure_ascii=False, indent=2)


def fetch_one_detail(legacy_id, fallback_name):
    try:
        resp = session.get(f'{BASE}/expediente/ver/{legacy_id}', timeout=30)
        resp.raise_for_status()
        return str(legacy_id), parse_patient_detail(resp.text, fallback_name)
    except Exception as e:
        return str(legacy_id), {'_error': str(e)}


def extract_paciente_details(rows, cache, max_workers=10):
    """Recorre /expediente/ver/{id} para enriquecer teléfono/email/etc."""
    to_fetch = []
    for r in rows:
        legacy_id = r.get('id')
        if not legacy_id:
            continue
        sid = str(legacy_id)
        # Solo re-extraer si no tenemos teléfono o no está cacheado
        if sid in cache and (cache[sid].get('celular') or cache[sid].get('whatsapp')):
            continue
        to_fetch.append((legacy_id, r.get('nombre', '')))

    log(f'Extrayendo detalles de {len(to_fetch)} pacientes con {max_workers} workers...')
    total = len(to_fetch)
    completed = 0
    with ThreadPoolExecutor(max_workers=max_workers) as ex:
        future_to_id = {ex.submit(fetch_one_detail, lid, name): lid for lid, name in to_fetch}
        for future in as_completed(future_to_id):
            lid, detail = future.result()
            if '_error' not in detail:
                cache[lid] = detail
            completed += 1
            if completed % 500 == 0:
                log(f'  {completed}/{total} detalles procesados')
                save_detail_cache(cache)
            time.sleep(0.01)  # pequeña pausa entre completados
    save_detail_cache(cache)
    log('Detalles de pacientes completados')


def extract_expedientes():
    rows = paginate('/expedientes/json', lambda s, l: dt_params(s, l), 'pacientes')
    cache = load_detail_cache()
    extract_paciente_details(rows, cache)

    out = []
    for r in rows:
        nombre = (r.get('nombre') or '').strip()
        if not nombre or nombre.upper() == '. . NO CITAR':
            continue
        detail = cache.get(str(r.get('id', '')), {})
        # Prioridad: celular > whatsapp > teléfono del listado
        phone = detail.get('celular') or detail.get('whatsapp') or (r.get('telefono') or '').strip()
        # Limpieza: quitar todo excepto dígitos; si empieza con 52 y tiene 12+ dígitos dejarlo, sino agregar prefijo MX si aplica
        phone = re.sub(r'\D', '', phone)
        if phone and not phone.startswith('52') and len(phone) == 10:
            phone = '52' + phone
        email = detail.get('email', '')
        # Algunos correos dummy los ignoramos
        if email.lower() in ('no@no.com', 'no@no'):
            email = ''
        out.append({
            'legacy_id': r.get('id', ''),
            'folio': r.get('folio', ''),
            'nombre': nombre,
            'apellido_paterno': '',
            'apellido_materno': '',
            'telefono': phone,
            'email': email,
            'fecha_nacimiento': detail.get('fecha_nacimiento', ''),
            'sexo': detail.get('sexo', ''),
            'medico_principal': r.get('medico_principal') or '',
            'etiquetas': ','.join(str(c) for c in (r.get('condiciones') or [])) if r.get('condiciones') else '',
            'vip': '1' if r.get('vip') else '0',
            'fuente': 'legacy',
        })
    save_csv('pacientes', out, [
        'legacy_id', 'folio', 'nombre', 'apellido_paterno', 'apellido_materno',
        'telefono', 'email', 'fecha_nacimiento', 'sexo', 'medico_principal',
        'etiquetas', 'vip', 'fuente'
    ])
    return out


def extract_monederos():
    rows = paginate('/monederos/json', lambda s, l: dt_params(s, l), 'monederos')
    out = []
    for r in rows:
        saldo = r.get('saldo') or {}
        phone = re.sub(r'\D', '', (r.get('celular') or '').strip())
        if phone and not phone.startswith('52') and len(phone) == 10:
            phone = '52' + phone
        out.append({
            'telefono': phone,
            'puntos': float(saldo.get('mn') or 0),
            'activo': '1' if r.get('activado') else '0',
        })
    save_csv('monederos', out, ['telefono', 'puntos', 'activo'])
    return out


def extract_crm():
    rows = paginate('/crm/prospectos/json', lambda s, l: dt_params(s, l), 'crm prospectos')
    out = []
    for r in rows:
        out.append({
            'nombre': r.get('nombre') or '',
            'folio': r.get('folio') or '',
            'status': r.get('status') or 0,
            'total_citas': r.get('total_citas') or 0,
            'siguiente_cita': r.get('siguiente_cita') or '',
        })
    save_csv('crm_prospectos', out, ['nombre', 'folio', 'status', 'total_citas', 'siguiente_cita'])
    return out


def extract_servicios():
    rows = paginate('/servicios/json', lambda s, l: dt_params(s, l), 'servicios')
    out = []
    for r in rows:
        precio_str = (r.get('precio_minimo') or '0').replace('$', '').replace('m.n.', '').replace(',', '').strip()
        try:
            precio = float(precio_str.split()[0])
        except Exception:
            precio = 0
        out.append({
            'codigo': f"S{r.get('id', 0):04d}",
            'nombre': r.get('servicio') or '',
            'categoria': (r.get('categoria') or '').lower().replace(' ', '_'),
            'precio': precio,
            'duracion_min': 30,
            'descripcion': r.get('grupo_texto') or '',
            'color': r.get('color') or '#1abc9c',
            'grupo': r.get('grupo_texto') or '',
            'favorito': '1' if r.get('favorito') else '0',
            'activo': '1' if r.get('activado') else '0',
        })
    save_csv('servicios', out, [
        'codigo', 'nombre', 'categoria', 'precio', 'duracion_min',
        'descripcion', 'color', 'grupo', 'favorito', 'activo'
    ])
    return out


def normalize_phone(phone):
    phone = re.sub(r'\D', '', (phone or ''))
    if phone and not phone.startswith('52') and len(phone) == 10:
        phone = '52' + phone
    return phone


def extract_ventas(start_date='2022-01-01', end_date='2026-07-06'):
    """Extrae ventas por mes y enriquece con teléfono del paciente."""
    log(f'Extrayendo ventas de {start_date} a {end_date}...')
    # Cargar mapeo legacy_id → teléfono desde pacientes.csv si ya existe
    phone_by_legacy = {}
    pac_path = OUTDIR / 'pacientes.csv'
    if pac_path.exists():
        with open(pac_path, newline='', encoding='utf-8') as f:
            for row in csv.DictReader(f):
                phone_by_legacy[row.get('legacy_id', '')] = row.get('telefono', '')

    all_rows = []
    current = datetime.strptime(start_date, '%Y-%m-%d').date()
    end = datetime.strptime(end_date, '%Y-%m-%d').date()
    while current <= end:
        chunk_end = min(current + timedelta(days=30), end)
        url = f"/ventas/json/{current.strftime('%Y-%m-%d')}/{chunk_end.strftime('%Y-%m-%d')}"
        try:
            data = fetch_json(url, dt_params(0, 10000))
            rows = data.get('data', [])
            all_rows.extend(rows)
            log(f'  {current} - {chunk_end}: {len(rows)} ventas (total: {len(all_rows)})')
        except Exception as e:
            log(f'  ERROR {current}-{chunk_end}: {e}')
        current = chunk_end + timedelta(days=1)
        time.sleep(0.5)

    out = []
    for r in all_rows:
        expediente_id = r.get('expedientes_id') or ''
        phone = normalize_phone(phone_by_legacy.get(str(expediente_id), ''))
        # Servicios vendidos: tomar del primer concepto; si hay varios, crear una línea por concepto
        conceptos = r.get('conceptos') or []
        if not conceptos:
            conceptos = [{'titulo': r.get('motivo') or 'Servicio', 'subtotal': r.get('subtotal') or 0}]
        for c in conceptos:
            precio = 0
            try:
                precio = float(c.get('subtotal') or c.get('pagado') or 0)
            except Exception:
                pass
            out.append({
                'legacy_id': r.get('id', ''),
                'expedientes_id': expediente_id,
                'paciente_telefono': phone,
                'paciente_nombre': r.get('nombre') or '',
                'fecha': (r.get('created_at') or '')[:10],
                'servicio_codigo': '',
                'servicio_nombre': c.get('titulo') or '',
                'cantidad': c.get('cantidad', 1) or 1,
                'precio_unitario': precio,
                'descuento': 0,
                'impuesto': 0,
                'estado': 'confirmed' if not r.get('cancelada') else 'cancelled',
                'notas': r.get('motivo') or '',
            })
    save_csv('ventas', out, [
        'legacy_id', 'expedientes_id', 'paciente_telefono', 'paciente_nombre', 'fecha',
        'servicio_codigo', 'servicio_nombre', 'cantidad', 'precio_unitario',
        'descuento', 'impuesto', 'estado', 'notas'
    ])
    return out


def extract_citas(start_date='2022-11-01', end_date='2026-07-06'):
    """Itera día a día sobre /agenda/citas/YYYY-MM-DD para obtener todas las citas."""
    log(f'Extrayendo citas de {start_date} a {end_date}...')
    phone_by_legacy = {}
    pac_path = OUTDIR / 'pacientes.csv'
    if pac_path.exists():
        with open(pac_path, newline='', encoding='utf-8') as f:
            for row in csv.DictReader(f):
                phone_by_legacy[row.get('legacy_id', '')] = row.get('telefono', '')

    # Mapeo de IDs de médicos del sistema legacy a nombres (de /agenda)
    medicos_map = {
        '32': 'Dra. Alejandra Ramos',
        '33': 'Dra. Lidia Martinez',
        '46': 'Dra. Fernanda Cerecer',
        '49': 'RODRIGUEZ FRIDA',
        '54': 'Dr. Benjamín Adrián',
        '79': 'XIMENA',
        '80': 'ELY',
    }

    all_citas = []
    current = datetime.strptime(start_date, '%Y-%m-%d').date()
    end = datetime.strptime(end_date, '%Y-%m-%d').date()
    days_with = 0
    while current <= end:
        url = f"/agenda/citas/{current.strftime('%Y-%m-%d')}"
        try:
            data = fetch_json(url)
            citas = data.get('citas', [])
            if citas:
                days_with += 1
            all_citas.extend(citas)
            if current.day == 1 or len(citas) > 0:
                log(f'  {current}: {len(citas)} citas (total acumulado: {len(all_citas)})')
        except Exception as e:
            log(f'  ERROR {current}: {e}')
        current += timedelta(days=1)
        time.sleep(0.05)

    out = []
    for c in all_citas:
        nombre = (c.get('nombre') or '').strip()
        if not nombre or nombre.upper().startswith('. . NO CITAR'):
            continue
        expediente_id = c.get('expedientes_id') or ''
        # El teléfono de la cita suele estar en el registro; si no, buscamos en pacientes
        phone = normalize_phone(c.get('telefono') or phone_by_legacy.get(str(expediente_id), ''))
        if not phone and c.get('whatsapp'):
            phone = normalize_phone(c.get('whatsapp'))
        email = (c.get('correo') or '').strip()
        if email.lower() in ('no@no.com', 'no@no'):
            email = ''
        inicio = c.get('inicio') or ''
        fin = c.get('fin') or ''
        # inicio/fin vienen como '1430' (HHMM)
        hora_str = ''
        if len(inicio) == 4:
            hora_str = f"{inicio[:2]}:{inicio[2:]}"
        elif len(inicio) == 3:
            hora_str = f"0{inicio[0]}:{inicio[1:]}"
        # Nombre del médico
        medico_id = str(c.get('medicos_id') or '')
        doctor_nombre = medicos_map.get(medico_id, medico_id if medico_id and medico_id != '0' else '')
        out.append({
            'legacy_id': c.get('id', ''),
            'expedientes_id': expediente_id,
            'paciente_telefono': phone,
            'paciente_nombre': nombre,
            'servicio_codigo': '',
            'servicio_nombre': c.get('titulo') or '',
            'fecha': c.get('fecha', ''),
            'hora': hora_str,
            'hora_fin': f"{fin[:2]}:{fin[2:]}" if len(fin) == 4 else '',
            'estado': 'confirmed',
            'doctor_nombre': doctor_nombre,
            'sucursal': 'rosarito',
            'medio': 'onsite',
            'notas': c.get('observaciones') or '',
            'estado_pago': 'not_paid',
            'monto_pagado': 0,
        })
    save_csv('citas', out, [
        'legacy_id', 'expedientes_id', 'paciente_telefono', 'paciente_nombre',
        'servicio_codigo', 'servicio_nombre', 'fecha', 'hora', 'hora_fin',
        'estado', 'doctor_nombre', 'sucursal', 'medio', 'notas',
        'estado_pago', 'monto_pagado'
    ])
    log(f'Citas: {len(out)} registros útiles en {days_with} días con citas')
    return out


def main():
    try:
        login()
    except Exception as e:
        log(f'ERROR login: {e}')
        sys.exit(1)

    extract_expedientes()
    extract_monederos()
    extract_crm()
    extract_servicios()
    extract_ventas()
    extract_citas()

    log('Extracción completada. Archivos en /root/migracion/extraccion/')


if __name__ == '__main__':
    main()
