import csv
import re
import sys
from collections import defaultdict
from datetime import datetime

CLIENT_NAME_MAP = {
    'UCSC': 'UCSC',
    'Universidad de Concepción': 'UDEC',
    'Universidad del Desarrollo': 'UDD',
    'Superintendencia de Salud': 'SUPERINTENDENCIA',
    'Mapatex S.A.': 'VERDEAGUA',
    'Rijk Zwaan': 'Rijk Zawaan',
    'PUC': 'PUC',
    'Universidad de Viña del Mar': 'UVM',
    'Catálogo Arquitectura': 'CATALOGOARQ',
    'Falabella': 'FALABELLA',
    'Pluxee': 'SODEXO',
    'e[ad] PUCV': 'PUCV',
    'DuocUC': 'DUOC',
    'Corporación Cultural Amereida': 'CORP. AMEREIDA',
    'Ministerio de Economía, Fomento y Turismo': 'MINECON',
    'ULTRAMAR': 'ULTRAMAR',
    'LIBERA TU CICLO': 'LIBERA TU CICLO',
}

CLIENT_SHORT_NAMES = {
    'Universidad de Concepción': 'UdeC',
    'Universidad del Desarrollo': 'UDD',
    'Superintendencia de Salud': 'Superdesalud',
    'Mapatex S.A.': 'Mapatex / VerdeAgua',
    'UCSC': 'UCSC',
    'e[ad] PUCV': 'e[ad] PUCV',
    'Pluxee': 'Pluxee',
    'Falabella': 'Falabella',
    'Ministerio de Economía, Fomento y Turismo': 'Min. Economía',
    'DuocUC': 'DuocUC',
    'Rijk Zwaan': 'Rijk Zwaan',
    'PUC': 'PUC',
    'Universidad de Viña del Mar': 'UVM',
    'Catálogo Arquitectura': 'CatArq',
}

def parse_time_csv(filepath, year):
    with open(filepath, 'r', encoding='utf-8') as f:
        content = f.read()

    lines = content.split('\n')
    records = []
    current_record = None

    for line in lines[1:]:
        if not line.strip():
            continue
        if line.startswith('TimeRecord,'):
            if current_record:
                records.append(current_record)
            current_record = line
        else:
            if current_record:
                current_record += '\n' + line

    if current_record:
        records.append(current_record)

    parsed = []
    for rec in records:
        rec_clean = rec.replace('\n', ' ')
        reader = csv.reader([rec_clean])
        for row in reader:
            if len(row) >= 17:
                parsed.append(row)

    data = []
    for row in parsed:
        try:
            date_str = row[11]
            if not re.match(r'\d{4}-\d{2}-\d{2}', date_str):
                continue
            date = datetime.strptime(date_str, '%Y-%m-%d')
            if year and date.year != year:
                continue

            user_name = row[13]
            client_name = row[10]
            project_name = row[8]
            task_name = row[6]
            value = float(row[16])
            status = row[17] if len(row) > 17 else ''
            is_billable = status == 'Billable'
            is_internal = client_name == 'Bloom User Experience'

            data.append({
                'date': date,
                'week': date.isocalendar()[1],
                'month': date.month,
                'user': user_name,
                'client': client_name,
                'project': project_name,
                'task': task_name,
                'hours': value,
                'billable': is_billable,
                'internal': is_internal
            })
        except (ValueError, IndexError):
            continue

    return data

def parse_money(s):
    if not s or s.strip() == '':
        return 0
    s = s.strip().replace('$', '').replace('.', '').replace(',', '').replace('"', '')
    try:
        return float(s)
    except ValueError:
        return 0

def parse_ingresos_csv(filepath, year):
    data = []
    with open(filepath, 'r', encoding='utf-8') as f:
        reader = csv.reader(f)
        header = next(reader)

        for row in reader:
            if len(row) < 12:
                continue

            fecha_str = row[4] if len(row) > 4 else ''
            mes_emision = row[6] if len(row) > 6 else ''

            try:
                if mes_emision and re.match(r'\d{4}\s+\d{2}', mes_emision):
                    parts = mes_emision.strip().split()
                    em_year = int(parts[0])
                    em_month = int(parts[1])
                elif fecha_str:
                    fecha_clean = fecha_str.strip()
                    if re.match(r'\d{2}-\d{2}-\d{4}', fecha_clean):
                        em_year = int(fecha_clean[6:10])
                        em_month = int(fecha_clean[3:5])
                    else:
                        continue
                else:
                    continue
            except (ValueError, IndexError):
                continue

            if em_year != year:
                continue

            nombre = row[0].strip() if len(row) > 0 else ''
            total = parse_money(row[7]) if len(row) > 7 else 0
            neto = parse_money(row[8]) if len(row) > 8 else 0
            iva = parse_money(row[9]) if len(row) > 9 else 0
            estado = row[10].strip() if len(row) > 10 else ''
            fecha_pago = row[11].strip() if len(row) > 11 else ''
            observaciones = row[14].strip() if len(row) > 14 else ''
            clasificacion = row[16].strip() if len(row) > 16 else ''
            tipo_proyecto = row[17].strip() if len(row) > 17 else ''

            pagado = estado.lower() == 'pagada'

            data.append({
                'nombre': nombre,
                'total': total,
                'neto': neto,
                'iva': iva,
                'estado': estado,
                'pagado': pagado,
                'mes_emision': em_month,
                'clasificacion': clasificacion,
                'tipo_proyecto': tipo_proyecto,
                'observaciones': observaciones,
            })

    return data

def calculate_metrics(data, ingresos_data, year, max_month=12):
    filtered_data = [d for d in data if d['month'] <= max_month]
    filtered_ingresos = [i for i in ingresos_data if i['mes_emision'] <= max_month]
    
    total_hours = sum(d['hours'] for d in filtered_data)
    billable_hours = sum(d['hours'] for d in filtered_data if d['billable'])
    internal_hours = sum(d['hours'] for d in filtered_data if d['internal'])
    external_hours = total_hours - internal_hours

    billable_pct = (billable_hours / total_hours * 100) if total_hours > 0 else 0
    internal_pct = (internal_hours / total_hours * 100) if total_hours > 0 else 0
    external_pct = (external_hours / total_hours * 100) if total_hours > 0 else 0

    users = set(d['user'] for d in filtered_data)

    person_month = defaultdict(lambda: defaultdict(float))
    person_week = defaultdict(lambda: defaultdict(float))
    person_internal = defaultdict(float)
    person_external = defaultdict(float)
    person_total = defaultdict(float)

    for d in filtered_data:
        person_month[d['user']][d['month']] += d['hours']
        person_week[d['user']][d['week']] += d['hours']
        person_total[d['user']] += d['hours']
        if d['internal']:
            person_internal[d['user']] += d['hours']
        else:
            person_external[d['user']] += d['hours']

    month_total = defaultdict(float)
    month_billable = defaultdict(float)
    month_internal = defaultdict(float)

    for d in filtered_data:
        month_total[d['month']] += d['hours']
        if d['billable']:
            month_billable[d['month']] += d['hours']
        if d['internal']:
            month_internal[d['month']] += d['hours']

    client_hours = defaultdict(float)
    for d in filtered_data:
        if not d['internal']:
            client_hours[d['client']] += d['hours']

    ingresos_by_client = defaultdict(lambda: {'total': 0, 'neto': 0, 'pagado': 0, 'pendiente': 0, 'facturas': 0, 'facturas_pagadas': 0})
    ingresos_month = defaultdict(float)
    ingresos_month_pagado = defaultdict(float)
    total_ingresos = 0
    total_ingresos_pagados = 0
    total_ingresos_pendientes = 0

    for ing in filtered_ingresos:
        nombre = ing['nombre']
        ingresos_by_client[nombre]['total'] += ing['total']
        ingresos_by_client[nombre]['neto'] += ing['neto']
        ingresos_by_client[nombre]['facturas'] += 1
        total_ingresos += ing['total']
        if ing['pagado']:
            ingresos_by_client[nombre]['pagado'] += ing['total']
            ingresos_by_client[nombre]['facturas_pagadas'] += 1
            total_ingresos_pagados += ing['total']
            ingresos_month_pagado[ing['mes_emision']] += ing['total']
        else:
            ingresos_by_client[nombre]['pendiente'] += ing['total']
            total_ingresos_pendientes += ing['total']
        ingresos_month[ing['mes_emision']] += ing['total']

    client_ingresos_mapped = {}
    for export_name, ing_name in CLIENT_NAME_MAP.items():
        if ing_name in ingresos_by_client:
            client_ingresos_mapped[export_name] = ingresos_by_client[ing_name]

    task_hours = defaultdict(lambda: {'hours': 0, 'internal': False, 'project': ''})
    for d in filtered_data:
        key = f"{d['task']}|{d['project']}"
        task_hours[key]['hours'] += d['hours']
        task_hours[key]['internal'] = d['internal']
        task_hours[key]['project'] = d['project']
        task_hours[key]['task'] = d['task']

    weeks_in_period = max_month * 4.33
    
    return {
        'year': year,
        'max_month': max_month,
        'weeks_in_period': weeks_in_period,
        'total': total_hours,
        'billable': billable_hours,
        'billable_pct': billable_pct,
        'internal': internal_hours,
        'internal_pct': internal_pct,
        'external': external_hours,
        'external_pct': external_pct,
        'users': sorted(users),
        'person_month': dict(person_month),
        'person_week': dict(person_week),
        'person_internal': dict(person_internal),
        'person_external': dict(person_external),
        'person_total': dict(person_total),
        'month_total': dict(month_total),
        'month_billable': dict(month_billable),
        'month_internal': dict(month_internal),
        'client_hours': dict(client_hours),
        'task_hours': dict(task_hours),
        'client_short_names': CLIENT_SHORT_NAMES,
        'ingresos_by_client': dict(ingresos_by_client),
        'client_ingresos_mapped': client_ingresos_mapped,
        'ingresos_month': dict(ingresos_month),
        'ingresos_month_pagado': dict(ingresos_month_pagado),
        'total_ingresos': total_ingresos,
        'total_ingresos_pagados': total_ingresos_pagados,
        'total_ingresos_pendientes': total_ingresos_pendientes,
    }

def format_num(n, decimals=1):
    return f"{n:,.{decimals}f}".replace(',', 'X').replace('.', ',').replace('X', '.')

def format_clp(n):
    n = int(round(n))
    s = f"{n:,}".replace(',', '.')
    return f"${s}"

def generate_html(metrics):
    year = metrics['year']
    max_month = metrics['max_month']
    weeks_in_period = metrics['weeks_in_period']
    month_names = ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic']
    month_names_full = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre']

    users_sorted = sorted(metrics['person_total'].keys(), key=lambda u: metrics['person_total'][u], reverse=True)
    total_persons = len(users_sorted)

    person_rows = []
    for user in users_sorted:
        months_data = [metrics['person_month'].get(user, {}).get(m, 0) for m in range(1, max_month + 1)]
        total = metrics['person_total'].get(user, 0)
        internal = metrics['person_internal'].get(user, 0)
        external = metrics['person_external'].get(user, 0)
        internal_pct = (internal / total * 100) if total > 0 else 0
        weeks_data = metrics['person_week'].get(user, {})
        active_weeks = len([w for w, h in weeks_data.items() if h > 0])
        prom_sem_bruto = total / weeks_in_period if total > 0 else 0
        prom_sem_ajustado = total / active_weeks if active_weeks > 0 else 0
        person_rows.append({
            'name': user, 'months': months_data, 'total': total,
            'internal': internal, 'external': external,
            'internal_pct': internal_pct,
            'prom_sem': prom_sem_bruto, 'prom_sem_ajustado': prom_sem_ajustado
        })

    month_totals = []
    for m in range(1, max_month + 1):
        t = metrics['month_total'].get(m, 0)
        b = metrics['month_billable'].get(m, 0)
        i = metrics['month_internal'].get(m, 0)
        month_totals.append({
            'name': month_names_full[m-1], 'total': t,
            'billable_pct': (b / t * 100) if t > 0 else 0,
            'internal_pct': (i / t * 100) if t > 0 else 0
        })

    clients_sorted = sorted(metrics['client_hours'].items(), key=lambda x: x[1], reverse=True)
    total_external = metrics['external']

    client_rows = []
    for client, hours in clients_sorted:
        pct = (hours / total_external * 100) if total_external > 0 else 0
        short_name = metrics['client_short_names'].get(client, client[:20])
        ingresos = metrics['client_ingresos_mapped'].get(client, {'total': 0, 'neto': 0, 'pagado': 0, 'pendiente': 0, 'facturas': 0, 'facturas_pagadas': 0})
        ingreso_total = ingresos['total']
        dolar_hora = (ingreso_total / hours) if hours > 0 else 0
        client_rows.append({
            'name': client, 'short_name': short_name,
            'hours': hours, 'pct': pct,
            'ingresos': ingreso_total, 'dolar_hora': dolar_hora,
            'facturas': ingresos['facturas'],
            'pagado': ingresos['pagado'], 'pendiente': ingresos['pendiente']
        })

    top3_external = sum(h for _, h in clients_sorted[:3])
    top3_pct = (top3_external / total_external * 100) if total_external > 0 else 0

    tasks_sorted = sorted(metrics['task_hours'].items(), key=lambda x: x[1]['hours'], reverse=True)[:15]
    total_prom_sem = metrics['total'] / weeks_in_period

    total_ingresos = metrics['total_ingresos']
    total_pagados = metrics['total_ingresos_pagados']
    total_pendientes = metrics['total_ingresos_pendientes']
    pct_cobrado = (total_pagados / total_ingresos * 100) if total_ingresos > 0 else 0
    valor_hora_promedio = (total_ingresos / metrics['external']) if metrics['external'] > 0 else 0
    horas_promedio_semana = metrics['total'] / weeks_in_period if weeks_in_period > 0 else 0

    ingresos_sorted = sorted(client_rows, key=lambda c: c['ingresos'], reverse=True)
    ingresos_con_datos = [c for c in ingresos_sorted if c['ingresos'] > 0]

    subtitle_text = f"Enero — Diciembre {year}" if max_month == 12 else f"Enero — Junio {year}"
    period_label = f"{year}" if max_month == 12 else f"H1 {year}"
    months_count = max_month

    logo_svg = '''<svg width="274" height="33" viewBox="0 0 274 33" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M11.9593 27.9611C18.3775 27.9611 22.4835 23.3333 22.4835 17.7722C22.4835 11.9 18.4173 7.07778 11.9593 7.07778C9.20866 7.07778 7.45463 8.16667 6.17897 9.60556H5.86006V0H0V27.3778H3.86684L4.74386 24.85H5.10264C6.45802 26.7944 8.96948 27.9611 11.9593 27.9611ZM11.2816 22.8278C8.13232 22.8278 5.74046 20.4167 5.74046 17.5389C5.74046 14.5444 8.21205 12.2111 11.2816 12.2111C14.3113 12.2111 16.7032 14.35 16.7032 17.5389C16.7032 20.65 14.2714 22.8278 11.2816 22.8278Z" fill="black"></path><path d="M25.2858 27.3778H31.1459V0H25.2858V27.3778Z" fill="black"></path><path d="M45.0844 28C51.3431 28 56.2863 23.4889 56.2863 17.5C56.2863 11.55 51.2634 7.07778 45.1243 7.07778C38.9851 7.07778 33.9224 11.55 33.9224 17.5C33.9224 23.4889 38.8257 28 45.0844 28ZM45.0844 22.7111C42.1344 22.7111 39.7824 20.4944 39.7824 17.5C39.7824 14.5444 42.1344 12.3278 45.1243 12.3278C48.1141 12.3278 50.4262 14.5444 50.4262 17.5C50.4262 20.4944 48.0742 22.7111 45.0844 22.7111Z" fill="black"></path><path d="M69.1244 28C75.3832 28 80.3263 23.4889 80.3263 17.5C80.3263 11.55 75.3034 7.07778 69.1643 7.07778C63.0252 7.07778 57.9624 11.55 57.9624 17.5C57.9624 23.4889 62.8657 28 69.1244 28ZM69.1244 22.7111C66.1745 22.7111 63.8225 20.4944 63.8225 17.5C63.8225 14.5444 66.1745 12.3278 69.1643 12.3278C72.1541 12.3278 74.4663 14.5444 74.4663 17.5C74.4663 20.4944 72.1143 22.7111 69.1244 22.7111Z" fill="black"></path><path d="M109.031 7.07778C105.722 7.07778 103.489 8.4 102.293 9.95556H102.094C100.818 8.16667 98.6658 7.07778 95.4368 7.07778C92.128 7.07778 89.9754 8.36111 88.8193 9.91667H88.4605L87.9024 7.62222H83.1187V27.3778H89.0984V17.1889C89.0984 14.5833 90.4936 12.3278 93.4436 12.3278C96.2341 12.3278 97.5496 13.9611 97.5496 16.6444V27.3778H103.529V16.7222C103.529 14.5056 104.924 12.3278 107.874 12.3278C110.426 12.3278 111.98 13.8444 111.98 16.6444V27.3778H118V15.3222C118 10.3833 114.572 7.07778 109.031 7.07778Z" fill="black"></path><circle cx="139" cy="16.5" r="12.5" fill="#FAD03E"></circle><circle cx="134.918" cy="13.4386" r="1.27551" fill="black"></circle><circle cx="143.082" cy="13.4386" r="1.27551" fill="black"></circle><path d="M138.87 23.8981C134.296 23.8981 131.949 19.6464 131.602 17.7756H133.152C133.498 19.3163 135.127 22.3975 138.87 22.3975C142.613 22.3975 144.415 19.3163 144.848 17.7756H146.398C145.965 19.6464 143.445 23.8981 138.87 23.8981Z" fill="black"></path><path d="M164.195 13.7587C162.049 13.7587 160.376 12.17 160.376 10.0238C160.376 7.87767 162.09 6.30288 164.195 6.30288C166.313 6.30288 168.027 7.87767 168.027 10.0238C168.027 12.17 166.341 13.7587 164.195 13.7587ZM164.195 12.1979C165.393 12.1979 166.341 11.292 166.341 10.0238C166.341 8.76959 165.407 7.8498 164.195 7.8498C162.996 7.8498 162.062 8.76959 162.062 10.0238C162.062 11.292 162.996 12.1979 164.195 12.1979Z" fill="black"></path><path d="M174.902 5.28554C174.33 5.28554 173.857 4.82564 173.857 4.24032C173.857 3.64107 174.358 3.18117 174.902 3.18117C175.473 3.18117 175.961 3.655 175.961 4.24032C175.961 4.81171 175.473 5.28554 174.902 5.28554ZM175.752 6.52586V13.5358H174.038V7.94735H171.487V13.5358H169.773V7.94735H168.84V6.52586H169.773V6.31682C169.773 4.56085 170.623 3.5017 172.421 3.5017C172.742 3.5017 173.034 3.52958 173.438 3.62713V4.89532H173.299C173.09 4.78383 172.881 4.75596 172.616 4.75596C171.864 4.75596 171.487 5.25766 171.487 6.12171V6.52586H175.752Z" fill="black"></path><path d="M181.016 13.7587C178.689 13.7587 177.17 12.1979 177.17 10.0378C177.17 7.86374 178.717 6.30288 181.002 6.30288C181.866 6.30288 182.619 6.51192 183.148 6.83246V8.3515H183.009C182.549 8.05884 181.866 7.8498 181.141 7.8498C179.831 7.8498 178.94 8.74172 178.94 10.0378C178.94 11.306 179.804 12.2118 181.169 12.2118C181.88 12.2118 182.549 12.0167 183.009 11.7241H183.148V13.2292C182.619 13.5497 181.88 13.7587 181.016 13.7587Z" fill="black"></path><path d="M185.447 5.28554C184.876 5.28554 184.402 4.82564 184.402 4.24032C184.402 3.64107 184.904 3.18117 185.447 3.18117C186.019 3.18117 186.52 3.655 186.52 4.24032C186.52 4.81171 186.032 5.28554 185.447 5.28554ZM184.583 13.5358V6.52586H186.311V13.5358H184.583Z" fill="black"></path><path d="M188.149 13.5358V6.52586H189.528L189.696 7.47352H189.807C190.197 6.86033 190.908 6.31682 192.106 6.31682C194.002 6.31682 195.047 7.50139 195.047 9.28523V13.5358H193.333V9.66151C193.333 8.5048 192.678 7.8498 191.633 7.8498C190.462 7.8498 189.877 8.79746 189.877 9.85661V13.5358H188.149Z" fill="black"></path><path d="M200.01 13.7448C197.822 13.7448 196.358 12.1282 196.358 10.0935C196.358 7.97522 197.822 6.30288 200.01 6.30288C201.375 6.30288 202.114 6.94395 202.532 7.55714H202.643L202.811 6.52586H204.051V11.2781C204.065 11.7937 204.051 12.1282 204.469 12.1282C204.539 12.1282 204.636 12.1143 204.734 12.0725H204.873L204.887 13.5497C204.608 13.6751 204.33 13.7448 204.023 13.7448C202.908 13.7448 202.755 13.1316 202.671 12.4487H202.56C202.114 13.1874 201.194 13.7448 200.01 13.7448ZM200.205 12.2397C201.487 12.2397 202.406 11.1806 202.406 10.0378C202.406 8.83927 201.459 7.82193 200.191 7.82193C198.978 7.82193 198.031 8.71384 198.031 10.0378C198.031 11.3339 198.992 12.2397 200.205 12.2397Z" fill="black"></path><path d="M212.639 13.7448C210.451 13.7448 208.988 12.1282 208.988 10.0935C208.988 7.97522 210.451 6.30288 212.639 6.30288C213.753 6.30288 214.492 6.83246 214.896 7.44565H215.008V3.72468H216.68V11.2781C216.694 11.7937 216.68 12.1282 217.098 12.1282C217.168 12.1282 217.265 12.1143 217.363 12.0725H217.502L217.516 13.5497C217.237 13.6751 216.959 13.7448 216.652 13.7448C215.537 13.7448 215.384 13.1316 215.3 12.4487H215.189C214.743 13.1874 213.823 13.7448 212.639 13.7448ZM212.834 12.2397C214.116 12.2397 215.036 11.1806 215.036 10.0378C215.036 8.83927 214.088 7.82193 212.82 7.82193C211.607 7.82193 210.66 8.71384 210.66 10.0378C210.66 11.3339 211.621 12.2397 212.834 12.2397Z" fill="black"></path><path d="M225.375 9.4664C225.375 9.8148 225.334 10.2468 225.208 10.5813H219.913C220.136 11.9192 221.25 12.393 222.435 12.393C223.355 12.393 224.051 12.2397 224.804 11.8774H224.943V13.1177C224.149 13.5776 223.285 13.7587 222.184 13.7587C219.857 13.7587 218.31 12.3094 218.31 10.0378C218.31 7.68256 219.927 6.30288 221.961 6.30288C224.051 6.30288 225.375 7.61288 225.375 9.4664ZM223.926 9.42459V9.27129C223.884 8.32363 223.201 7.57107 221.975 7.57107C221 7.57107 220.136 8.12852 219.913 9.42459H223.926Z" fill="black"></path><path d="M233.229 13.7448C231.041 13.7448 229.578 12.1282 229.578 10.0935C229.578 7.97522 231.041 6.30288 233.229 6.30288C234.344 6.30288 235.082 6.83246 235.487 7.44565H235.598V3.72468H237.27V11.2781C237.284 11.7937 237.27 12.1282 237.688 12.1282C237.758 12.1282 237.856 12.1143 237.953 12.0725H238.093L238.106 13.5497C237.828 13.6751 237.549 13.7448 237.242 13.7448C236.128 13.7448 235.974 13.1316 235.891 12.4487H235.779C235.333 13.1874 234.414 13.7448 233.229 13.7448ZM233.424 12.2397C234.706 12.2397 235.626 11.1806 235.626 10.0378C235.626 8.83927 234.678 7.82193 233.41 7.82193C232.198 7.82193 231.25 8.71384 231.25 10.0378C231.25 11.3339 232.212 12.2397 233.424 12.2397Z" fill="black"></path><path d="M240.25 5.28554C239.679 5.28554 239.205 4.82564 239.205 4.24032C239.205 3.64107 239.707 3.18117 240.25 3.18117C240.822 3.18117 241.324 3.655 241.324 4.24032C241.324 4.81171 240.836 5.28554 240.25 5.28554ZM239.386 13.5358V6.52586H241.114V13.5358H239.386Z" fill="black"></path><path d="M246.241 9.28523C247.69 9.57789 248.763 10.0796 248.763 11.5568C248.763 12.8947 247.62 13.7448 245.572 13.7448C244.067 13.7448 242.938 13.257 242.618 13.0759V11.738H242.757C243.523 12.1421 244.638 12.4766 245.642 12.4766C246.631 12.4766 247.091 12.1282 247.091 11.5986C247.091 10.9297 246.422 10.8321 245.614 10.6649C243.565 10.2886 242.618 9.73119 242.618 8.39331C242.618 6.95788 243.914 6.31682 245.502 6.31682C246.52 6.31682 247.607 6.55373 248.275 6.90214V8.21214H248.136C247.467 7.83586 246.45 7.50139 245.516 7.50139C244.75 7.50139 244.29 7.75225 244.29 8.28182C244.29 8.95076 244.945 9.03438 246.241 9.28523Z" fill="black"></path><path d="M256.758 9.4664C256.758 9.8148 256.716 10.2468 256.59 10.5813H251.295C251.518 11.9192 252.633 12.393 253.817 12.393C254.737 12.393 255.434 12.2397 256.186 11.8774H256.326V13.1177C255.531 13.5776 254.667 13.7587 253.566 13.7587C251.239 13.7587 249.692 12.3094 249.692 10.0378C249.692 7.68256 251.309 6.30288 253.343 6.30288C255.434 6.30288 256.758 7.61288 256.758 9.4664ZM255.308 9.42459V9.27129C255.267 8.32363 254.584 7.57107 253.357 7.57107C252.382 7.57107 251.518 8.12852 251.295 9.42459H255.308Z" fill="black"></path><path d="M262.892 5.17405C261.86 5.17405 261.373 4.25426 260.578 4.25426C260.105 4.25426 260.007 4.60266 259.979 5.03469H258.878C258.878 3.8083 259.533 3 260.578 3C261.596 3 262.069 3.91979 262.878 3.91979C263.352 3.91979 263.463 3.59926 263.491 3.18117H264.606C264.606 4.43543 264.076 5.17405 262.892 5.17405ZM258.14 13.5358V6.52586H259.519L259.687 7.47352H259.798C260.188 6.86033 260.899 6.31682 262.097 6.31682C263.993 6.31682 265.038 7.50139 265.038 9.28523V13.5358H263.324V9.66151C263.324 8.5048 262.669 7.8498 261.624 7.8498C260.453 7.8498 259.868 8.79746 259.868 9.85661V13.5358H258.14Z" fill="black"></path><path d="M270.168 13.7587C268.022 13.7587 266.349 12.17 266.349 10.0238C266.349 7.87767 268.063 6.30288 270.168 6.30288C272.286 6.30288 274 7.87767 274 10.0238C274 12.17 272.314 13.7587 270.168 13.7587ZM270.168 12.1979C271.366 12.1979 272.314 11.292 272.314 10.0238C272.314 8.76959 271.38 7.8498 270.168 7.8498C268.969 7.8498 268.036 8.76959 268.036 10.0238C268.036 11.292 268.969 12.1979 270.168 12.1979Z" fill="black"></path><path d="M161.38 33C161.003 33 160.808 32.9861 160.223 32.791V31.3834H160.362C160.627 31.481 160.906 31.5367 161.212 31.5367C162.216 31.5367 162.662 30.7841 162.913 29.8922L160 22.7848H161.826L163.79 28.0945H163.888L165.769 22.7848H167.595L164.641 30.0177C163.958 31.6761 163.247 33 161.38 33Z" fill="black"></path><path d="M174.778 30.0037C172.591 30.0037 171.127 28.3871 171.127 26.3524C171.127 24.2341 172.591 22.5618 174.778 22.5618C175.893 22.5618 176.632 23.0914 177.036 23.7046H177.148V19.9836H178.82V27.537C178.834 28.0526 178.82 28.3871 179.238 28.3871C179.308 28.3871 179.405 28.3732 179.503 28.3314H179.642L179.656 29.8086C179.377 29.934 179.099 30.0037 178.792 30.0037C177.677 30.0037 177.524 29.3905 177.44 28.7076H177.329C176.883 29.4463 175.963 30.0037 174.778 30.0037ZM174.974 28.4986C176.256 28.4986 177.175 27.4395 177.175 26.2967C177.175 25.0982 176.228 24.0808 174.96 24.0808C173.747 24.0808 172.8 24.9727 172.8 26.2967C172.8 27.5928 173.761 28.4986 174.974 28.4986Z" fill="black"></path><path d="M187.515 25.7253C187.515 26.0737 187.473 26.5057 187.348 26.8402H182.052C182.275 28.1781 183.39 28.6519 184.575 28.6519C185.495 28.6519 186.191 28.4986 186.944 28.1363H187.083V29.3766C186.289 29.8365 185.425 30.0177 184.324 30.0177C181.997 30.0177 180.45 28.5683 180.45 26.2967C180.45 23.9415 182.066 22.5618 184.101 22.5618C186.191 22.5618 187.515 23.8718 187.515 25.7253ZM186.066 25.6835V25.5302C186.024 24.5825 185.341 23.83 184.115 23.83C183.139 23.83 182.275 24.3874 182.052 25.6835H186.066Z" fill="black"></path><path d="M192.186 25.5441C193.635 25.8368 194.708 26.3385 194.708 27.8157C194.708 29.1536 193.566 30.0037 191.517 30.0037C190.012 30.0037 188.883 29.5159 188.563 29.3348V27.9969H188.702C189.469 28.4011 190.583 28.7355 191.587 28.7355C192.576 28.7355 193.036 28.3871 193.036 27.8575C193.036 27.1886 192.367 27.0911 191.559 26.9238C189.51 26.5475 188.563 25.9901 188.563 24.6522C188.563 23.2168 189.859 22.5757 191.447 22.5757C192.465 22.5757 193.552 22.8126 194.221 23.161V24.471H194.081C193.412 24.0948 192.395 23.7603 191.461 23.7603C190.695 23.7603 190.235 24.0111 190.235 24.5407C190.235 25.2097 190.89 25.2933 192.186 25.5441Z" fill="black"></path><path d="M199.275 30.0037C197.087 30.0037 195.624 28.3871 195.624 26.3524C195.624 24.2341 197.087 22.5618 199.275 22.5618C200.64 22.5618 201.379 23.2028 201.797 23.816H201.908L202.076 22.7848H203.316V27.537C203.33 28.0526 203.316 28.3871 203.734 28.3871C203.804 28.3871 203.901 28.3732 203.999 28.3314H204.138L204.152 29.8086C203.873 29.934 203.595 30.0037 203.288 30.0037C202.173 30.0037 202.02 29.3905 201.936 28.7076H201.825C201.379 29.4463 200.459 30.0037 199.275 30.0037ZM199.47 28.4986C200.752 28.4986 201.672 27.4395 201.672 26.2967C201.672 25.0982 200.724 24.0808 199.456 24.0808C198.243 24.0808 197.296 24.9727 197.296 26.2967C197.296 27.5928 198.257 28.4986 199.47 28.4986Z" fill="black"></path><path d="M205.432 29.7947V22.7848H206.756L206.937 23.7324H207.035C207.439 23.0914 208.205 22.5897 209.153 22.5757H209.39V24.1923H209.153C208.066 24.1923 207.104 24.8891 207.104 26.3803V29.7947H205.432Z" fill="black"></path><path d="M210.522 29.7947V22.7848H211.846L212.027 23.7324H212.124C212.529 23.0914 213.295 22.5897 214.243 22.5757H214.48V24.1923H214.243C213.156 24.1923 212.194 24.8891 212.194 26.3803V29.7947H210.522Z" fill="black"></path><path d="M218.767 30.0177C216.621 30.0177 214.948 28.4289 214.948 26.2828C214.948 24.1366 216.662 22.5618 218.767 22.5618C220.885 22.5618 222.599 24.1366 222.599 26.2828C222.599 28.4289 220.913 30.0177 218.767 30.0177ZM218.767 28.4568C219.965 28.4568 220.913 27.5509 220.913 26.2828C220.913 25.0285 219.979 24.1087 218.767 24.1087C217.568 24.1087 216.634 25.0285 216.634 26.2828C216.634 27.5509 217.568 28.4568 218.767 28.4568Z" fill="black"></path><path d="M224.008 29.7947V19.9836H225.695V29.7947H224.008Z" fill="black"></path><path d="M227.533 29.7947V19.9836H229.219V29.7947H227.533Z" fill="black"></path><path d="M234.444 30.0177C232.298 30.0177 230.626 28.4289 230.626 26.2828C230.626 24.1366 232.34 22.5618 234.444 22.5618C236.562 22.5618 238.276 24.1366 238.276 26.2828C238.276 28.4289 236.59 30.0177 234.444 30.0177ZM234.444 28.4568C235.643 28.4568 236.59 27.5509 236.59 26.2828C236.59 25.0285 235.657 24.1087 234.444 24.1087C233.246 24.1087 232.312 25.0285 232.312 26.2828C232.312 27.5509 233.246 28.4568 234.444 28.4568Z" fill="black"></path><path d="M244.784 29.7947L241.83 22.7848H243.544L245.272 27.1329H245.37L247.098 22.7848H248.031L249.759 27.1329H249.857L251.599 22.7848H253.299L250.345 29.7947H249.467L247.613 25.2654H247.516L245.662 29.7947H244.784Z" fill="black"></path><path d="M260.704 25.7253C260.704 26.0737 260.662 26.5057 260.537 26.8402H255.241C255.464 28.1781 256.579 28.6519 257.764 28.6519C258.684 28.6519 259.38 28.4986 260.133 28.1363H260.272V29.3766C259.478 29.8365 258.614 30.0177 257.513 30.0177C255.186 30.0177 253.639 28.5683 253.639 26.2967C253.639 23.9415 255.255 22.5618 257.29 22.5618C259.38 22.5618 260.704 23.8718 260.704 25.7253ZM259.255 25.6835V25.5302C259.213 24.5825 258.53 23.83 257.304 23.83C256.328 23.83 255.464 24.3874 255.241 25.6835H259.255Z" fill="black"></path><path d="M266.142 30.0037C264.957 30.0037 264.023 29.4463 263.577 28.7076H263.48L263.243 29.7947H262.086V19.9836H263.772V23.7046H263.884C264.288 23.0914 265.027 22.5618 266.128 22.5618C268.329 22.5618 269.779 24.2341 269.779 26.3524C269.779 28.3871 268.315 30.0037 266.142 30.0037ZM265.946 28.4986C267.145 28.4986 268.12 27.5928 268.12 26.2967C268.12 24.9727 267.159 24.0808 265.96 24.0808C264.692 24.0808 263.731 25.0982 263.731 26.2967C263.731 27.4395 264.664 28.4986 265.946 28.4986Z" fill="black"></path></svg>'''

    html = f'''<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Informe {period_label} — Bloom</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&family=Source+Sans+3:ital,wght@0,300;0,400;0,600;0,700;1,400&display=swap" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js"></script>
<style>
*, *::before, *::after {{ box-sizing: border-box; margin: 0; padding: 0; }}
:root {{
  --blue:    #1350ee;
  --orange:  #ff6b00;
  --red:     #ee3333;
  --green:   #30934b;
  --yellow:  #fad03e;
  --black:   #000000;
  --gray-50: #f7f7f7;
  --gray-100:#eeeeee;
  --gray-400:#999999;
  --gray-600:#555555;
  --white:   #ffffff;
  --font-body: 'Source Sans 3', sans-serif;
  --font-label:'Roboto', sans-serif;
}}
body {{ font-family: var(--font-body); background: var(--white); color: var(--black); font-size: 15px; line-height: 1.5; }}
.page-header-bar {{ border-bottom: 1px solid var(--gray-100); }}
.page-header {{ display: flex; align-items: center; justify-content: space-between; max-width: 1200px; margin: 0 auto; padding: 2rem 3rem; }}
.header-left h1 {{ font-family: var(--font-body); font-weight: 700; font-size: 32px; letter-spacing: -.02em; margin-bottom: .2rem; }}
.header-left .subtitle {{ font-family: var(--font-label); font-size: 14px; color: var(--gray-400); font-weight: 400; }}
.bloom-logo svg {{ height: 24px; width: auto; display: block; }}
.kpi-bar {{ border-bottom: 1px solid var(--gray-100); }}
.kpi-row {{ display: grid; grid-template-columns: repeat(4, 1fr); max-width: 1200px; margin: 0 auto; padding: 1.75rem 3rem 1.5rem; gap: 1rem; }}
.kpi-label {{ font-family: var(--font-label); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; color: var(--gray-400); margin-bottom: .3rem; }}
.kpi-value {{ font-family: var(--font-body); font-weight: 700; font-size: 30px; letter-spacing: -.02em; line-height: 1; }}
.kpi-sub {{ font-size: 12px; color: var(--gray-400); margin-top: .25rem; }}
main {{ max-width: 1200px; margin: 0 auto; padding: 2.5rem 3rem 4rem; }}
section {{ margin-bottom: 3rem; }}
.section-label {{ font-family: var(--font-label); font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: .1em; color: var(--gray-400); margin-bottom: 1.25rem; padding-bottom: .5rem; border-bottom: 1px solid var(--gray-100); }}
.table-wrap {{ overflow-x: auto; }}
table {{ width: 100%; border-collapse: collapse; font-size: 14px; }}
thead th {{ background: var(--black); color: var(--white); font-family: var(--font-label); font-size: 11px; font-weight: 500; letter-spacing: .05em; text-transform: uppercase; padding: .75rem 1rem; text-align: left; }}
thead th:not(:first-child) {{ text-align: right; }}
tbody td {{ padding: .65rem 1rem; border-bottom: 1px solid var(--gray-100); color: var(--black); }}
tbody td:not(:first-child) {{ text-align: right; font-variant-numeric: tabular-nums; }}
tbody tr:nth-child(odd) td {{ background: var(--gray-50); }}
tbody tr:nth-child(even) td {{ background: var(--white); }}
tbody tr:hover td {{ background: #eef2fd; }}
tfoot td {{ font-weight: 700; background: var(--gray-100); padding: .65rem 1rem; border-top: 2px solid var(--black); font-size: 14px; }}
tfoot td:not(:first-child) {{ text-align: right; }}
.name-cell {{ font-weight: 600; }}
.pct-badge {{ display: inline-block; font-size: 10px; padding: 1px 6px; border-radius: 3px; margin-left: 5px; font-weight: 500; font-family: var(--font-label); }}
.pct-high {{ background: #fff0e6; color: #c94400; }}
.pct-low  {{ background: #e8f4ec; color: #1e6635; }}
.flag {{ color: var(--orange); font-weight: 700; }}
.chart-box {{ padding: 1.25rem 0; }}
.chart-wrap {{ position: relative; }}
.legend {{ display: flex; gap: 18px; margin-bottom: 12px; flex-wrap: wrap; }}
.legend span {{ display: flex; align-items: center; gap: 6px; font-family: var(--font-label); font-size: 12px; color: var(--gray-600); }}
.legend-sq {{ width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0; }}
.two-col {{ display: grid; grid-template-columns: 1fr 1fr; gap: 2.5rem; align-items: start; }}
.alert {{ border-left: 3px solid var(--blue); padding: .9rem 1.1rem; background: #f0f4fe; margin-top: 1rem; }}
.alert-title {{ font-weight: 700; font-size: 14px; margin-bottom: .3rem; }}
.alert-body {{ font-size: 13px; color: var(--gray-600); line-height: 1.6; }}
.note {{ font-size: 12px; color: var(--gray-400); margin-top: .6rem; font-family: var(--font-label); }}
footer {{ font-family: var(--font-label); font-size: 11px; color: var(--gray-400); text-align: center; padding: 2rem 3rem; border-top: 1px solid var(--gray-100); }}
@media (max-width: 720px) {{
  .page-header, .kpi-row, main {{ padding-left: 1.25rem; padding-right: 1.25rem; }}
  .kpi-row {{ grid-template-columns: 1fr 1fr; }}
  .two-col {{ grid-template-columns: 1fr; }}
}}
</style>
</head>
<body>

<div class="page-header-bar">
<div class="page-header">
  <div class="header-left">
    <h1>Informe de Horas {period_label}</h1>
    <div class="subtitle">{subtitle_text}</div>
  </div>
  <div class="bloom-logo">{logo_svg}</div>
</div>
</div>

<div class="kpi-bar">
<div class="kpi-row">
  <div>
    <div class="kpi-label">Total horas equipo</div>
    <div class="kpi-value">{format_num(metrics["total"], 0).replace(',0', '')}</div>
    <div class="kpi-sub">{total_persons} personas · {months_count} meses</div>
  </div>
  <div>
    <div class="kpi-label">Billable</div>
    <div class="kpi-value">{format_num(metrics["billable_pct"], 1)}%</div>
    <div class="kpi-sub">{format_num(metrics["billable"], 0).replace(',0', '')} hrs facturables</div>
  </div>
  <div>
    <div class="kpi-label">Horas a clientes</div>
    <div class="kpi-value">{format_num(metrics["external"], 0).replace(',0', '')}</div>
    <div class="kpi-sub">{format_num(metrics["external_pct"], 1)}% del total</div>
  </div>
  <div>
    <div class="kpi-label">Trabajo interno</div>
    <div class="kpi-value">{format_num(metrics["internal"], 0).replace(',0', '')}</div>
    <div class="kpi-sub">{format_num(metrics["internal_pct"], 1)}% del total</div>
  </div>
</div>
</div>

<div class="kpi-bar">
<div class="kpi-row">
  <div>
    <div class="kpi-label">Ingresos facturados</div>
    <div class="kpi-value">{format_clp(total_ingresos)}</div>
    <div class="kpi-sub">{len(ingresos_con_datos)} clientes facturados</div>
  </div>
  <div>
    <div class="kpi-label">Valor hora promedio</div>
    <div class="kpi-value">{format_clp(total_ingresos / total_external if total_external > 0 else 0)}</div>
    <div class="kpi-sub">Ingresos / horas externas</div>
  </div>
  <div>
    <div class="kpi-label">Horas promedio por semana</div>
    <div class="kpi-value">{format_num(metrics["total"] / weeks_in_period, 1)}</div>
    <div class="kpi-sub">{format_num(weeks_in_period, 0)} semanas en el período</div>
  </div>
  <div>
    <div class="kpi-label">Ticket promedio</div>
    <div class="kpi-value">{format_clp(total_ingresos / sum(c['facturas'] for c in ingresos_con_datos) if ingresos_con_datos else 0)}</div>
    <div class="kpi-sub">{sum(c['facturas'] for c in ingresos_con_datos)} facturas en total</div>
  </div>
</div>
</div>

<main>

  <section>
    <div class="section-label">Horas por persona</div>
    <div class="legend">
      <span><span class="legend-sq" style="background:#1350ee"></span>Horas externas</span>
      <span><span class="legend-sq" style="background:#ff6b00"></span>Horas internas</span>
    </div>
    <div class="chart-wrap" style="height:260px; margin-bottom: 1.5rem">
      <canvas id="chartPersonas"></canvas>
    </div>
    <div class="table-wrap">
      <table>
        <thead>
          <tr>
            <th>Persona</th>
'''
    for mn in month_names[:max_month]:
        html += f'            <th>{mn}</th>\n'
    html += '''            <th>Total</th>
            <th>Prom. sem.</th>
            <th>Prom. sem. ajustado</th>
            <th>Internas</th>
            <th>Externas</th>
          </tr>
        </thead>
        <tbody>
'''

    for pr in person_rows:
        badge_class = 'pct-high' if pr['internal_pct'] >= 20 else 'pct-low'
        html += f'          <tr>\n            <td class="name-cell">{pr["name"]}</td>\n'
        for m_val in pr['months']:
            html += f'            <td>{format_num(m_val, 1)}</td>\n'
        html += f'''            <td><strong>{format_num(pr["total"], 1)}</strong></td>
            <td>{format_num(pr["prom_sem"], 1)}</td>
            <td>{format_num(pr["prom_sem_ajustado"], 1)}</td>
            <td>{format_num(pr["internal"], 1)} <span class="pct-badge {badge_class}">{format_num(pr["internal_pct"], 0)}%</span></td>
            <td>{format_num(pr["external"], 1)}</td>
          </tr>
'''

    html += '        </tbody>\n        <tfoot>\n          <tr>\n            <td>Total</td>\n'
    for m in range(1, max_month + 1):
        html += f'            <td>{format_num(metrics["month_total"].get(m, 0), 1)}</td>\n'
    html += f'''            <td>{format_num(metrics["total"], 1)}</td>
            <td>{format_num(total_prom_sem, 1)}</td>
            <td>—</td>
            <td>{format_num(metrics["internal"], 1)}</td>
            <td>{format_num(metrics["external"], 1)}</td>
          </tr>
        </tfoot>
      </table>
    </div>
    <p class="note">Valores en naranja indican caídas significativas respecto al promedio mensual. % interno = horas internas / total. Prom. sem.: base 52 semanas brutas. Prom. sem. ajustado: excluye semanas con 0 hrs registradas (vacaciones identificadas).</p>
  </section>

  <section>
    <div class="section-label">Volumen mensual</div>
    <div class="two-col">
      <div>
        <div class="legend">
          <span><span class="legend-sq" style="background:#1350ee"></span>Externas</span>
          <span><span class="legend-sq" style="background:#cccccc"></span>Internas</span>
        </div>
        <div class="chart-wrap" style="height:220px">
          <canvas id="chartMensual"></canvas>
        </div>
      </div>
      <div class="table-wrap">
        <table>
          <thead><tr><th>Mes</th><th>Total hrs</th><th>Billable</th><th>Internas</th></tr></thead>
          <tbody>
'''
    for mt in month_totals:
        html += f'            <tr><td>{mt["name"]}</td><td>{format_num(mt["total"], 1)}</td><td>{format_num(mt["billable_pct"], 0)}%</td><td>{format_num(mt["internal_pct"], 0)}%</td></tr>\n'

    html += f'''          </tbody>
          <tfoot><tr><td>{period_label}</td><td>{format_num(metrics["total"], 1)}</td><td>{format_num(metrics["billable_pct"], 1)}%</td><td>{format_num(metrics["internal_pct"], 1)}%</td></tr></tfoot>
        </table>
      </div>
    </div>
  </section>

  <section>
    <div class="section-label">Clientes: horas e ingresos</div>
    <div class="table-wrap">
      <table>
        <thead>
          <tr>
            <th>Cliente</th>
            <th>Horas</th>
            <th>% horas</th>
            <th>Ingresos</th>
            <th>$/hora</th>
            <th>Facturas</th>
          </tr>
        </thead>
        <tbody>
'''
    for cr in ingresos_con_datos:
        html += f'''          <tr>
            <td class="name-cell">{cr["short_name"]}</td>
            <td>{format_num(cr["hours"], 1)}</td>
            <td>{format_num(cr["pct"], 1)}%</td>
            <td>{format_clp(cr["ingresos"])}</td>
            <td>{format_clp(cr["dolar_hora"])}</td>
            <td>{cr["facturas"]}</td>
          </tr>
'''

    total_facturas = sum(c['facturas'] for c in ingresos_con_datos)
    avg_dolar_hora = (total_ingresos / total_external) if total_external > 0 else 0

    html += f'''        </tbody>
        <tfoot>
          <tr>
            <td>Total</td>
            <td>{format_num(total_external, 1)}</td>
            <td>100%</td>
            <td>{format_clp(total_ingresos)}</td>
            <td>{format_clp(avg_dolar_hora)}</td>
            <td>{total_facturas}</td>
          </tr>
        </tfoot>
      </table>
    </div>
    <p class="note">$/hora = ingresos totales / horas externas por cliente.</p>
  </section>

  <section>
    <div class="section-label">Concentración de clientes externos</div>
    <div class="two-col">
      <div>
        <div class="chart-wrap" style="height:320px">
          <canvas id="chartClientes"></canvas>
        </div>
      </div>
      <div>
        <div class="table-wrap">
          <table>
            <thead><tr><th>Cliente</th><th>Horas</th><th>% externo</th></tr></thead>
            <tbody>
'''
    for cr in client_rows[:15]:
        html += f'              <tr><td>{cr["name"]}</td><td>{format_num(cr["hours"], 1)}</td><td>{format_num(cr["pct"], 1)}%</td></tr>\n'

    html += f'''            </tbody>
            <tfoot><tr><td>Total externo</td><td>{format_num(total_external, 1)}</td><td>100%</td></tr></tfoot>
          </table>
        </div>
        <div class="alert">
          <div class="alert-title">{format_num(top3_pct, 1)}% de las horas externas en 3 clientes</div>
          <div class="alert-body">Los 3 principales clientes concentran {format_num(top3_pct, 1)}% de la carga externa del periodo.</div>
        </div>
      </div>
    </div>
  </section>

  <section>
    <div class="section-label">Tareas con mayor demanda de horas</div>
    <div class="legend">
      <span><span class="legend-sq" style="background:#1350ee"></span>Proyecto cliente</span>
      <span><span class="legend-sq" style="background:#ff6b00"></span>Proyecto interno Bloom</span>
    </div>
    <div class="chart-wrap" style="height:460px">
      <canvas id="chartTareas"></canvas>
    </div>
  </section>

</main>

<footer>Bloom User Experience SpA. &nbsp;·&nbsp; Informe {period_label} &nbsp;·&nbsp; Datos extraídos de ActiveCollab</footer>

<script>
const blue   = '#1350ee';
const blueL  = '#7fa3f5';
const orange = '#ff6b00';
const green  = '#30934b';
const gray   = '#cccccc';
const muted  = '#999999';
const gridC  = '#eeeeee';

Chart.defaults.font.family = "'Source Sans 3', sans-serif";

new Chart(document.getElementById('chartPersonas'), {{
  type: 'bar',
  data: {{
    labels: {str([pr["name"].split()[0] for pr in person_rows])},
    datasets: [
      {{ label: 'Externas', data: {str([round(pr["external"], 1) for pr in person_rows])}, backgroundColor: blue, stack:'s', borderRadius:{{topLeft:0,topRight:0}}, borderSkipped:'bottom' }},
      {{ label: 'Internas', data: {str([round(pr["internal"], 1) for pr in person_rows])}, backgroundColor: orange, stack:'s', borderRadius:{{topLeft:3,topRight:3}}, borderSkipped:'bottom' }}
    ]
  }},
  options: {{
    responsive:true, maintainAspectRatio:false,
    plugins:{{ legend:{{display:false}}, tooltip:{{callbacks:{{label: ctx => ' '+ctx.raw+' hrs ('+ctx.dataset.label+')'}}}} }},
    scales:{{
      x:{{ stacked:true, grid:{{display:false}}, ticks:{{color:muted,font:{{size:12}}}}, border:{{color:gridC}} }},
      y:{{ stacked:true, grid:{{color:gridC}}, ticks:{{color:muted,font:{{size:11}},callback:v=>v+' h'}}, border:{{color:gridC}} }}
    }}
  }}
}});

new Chart(document.getElementById('chartMensual'), {{
  type: 'bar',
  data: {{
    labels: {str(month_names[:max_month])},
    datasets: [
      {{ label:'Externas', data:{str([round(metrics["month_total"].get(m, 0) - metrics["month_internal"].get(m, 0), 1) for m in range(1, max_month + 1)])}, backgroundColor:blue, stack:'s', borderRadius:{{topLeft:0,topRight:0}}, borderSkipped:'bottom' }},
      {{ label:'Internas', data:{str([round(metrics["month_internal"].get(m, 0), 1) for m in range(1, max_month + 1)])}, backgroundColor:gray, stack:'s', borderRadius:{{topLeft:3,topRight:3}}, borderSkipped:'bottom' }}
    ]
  }},
  options: {{
    responsive:true, maintainAspectRatio:false,
    plugins:{{ legend:{{display:false}}, tooltip:{{callbacks:{{label: ctx => ' '+ctx.raw+' hrs ('+ctx.dataset.label+')'}}}} }},
    scales:{{
      x:{{ stacked:true, grid:{{display:false}}, ticks:{{color:muted,font:{{size:12}}}}, border:{{color:gridC}} }},
      y:{{ stacked:true, grid:{{color:gridC}}, ticks:{{color:muted,font:{{size:11}},callback:v=>v+' h'}}, border:{{color:gridC}} }}
    }}
  }}
}});

const clientes = {str([cr["short_name"] for cr in client_rows[:12]])};
const pcts     = {str([round(cr["pct"], 1) for cr in client_rows[:12]])};
const cliCol   = pcts.map((_,i) => i < 3 ? blue : i < 7 ? blueL : gray);

new Chart(document.getElementById('chartClientes'), {{
  type:'bar',
  data:{{ labels:clientes, datasets:[{{ data:pcts, backgroundColor:cliCol, borderRadius:{{topRight:3,bottomRight:3}}, borderSkipped:'left' }}] }},
  options:{{
    indexAxis:'y', responsive:true, maintainAspectRatio:false,
    plugins:{{ legend:{{display:false}}, tooltip:{{callbacks:{{label: ctx=>' '+ctx.raw+'%'}}}} }},
    scales:{{
      x:{{ grid:{{color:gridC}}, ticks:{{color:muted,font:{{size:11}},callback:v=>v+'%'}}, border:{{color:gridC}} }},
      y:{{ grid:{{display:false}}, ticks:{{color:muted,font:{{size:12}}}}, border:{{color:gridC}} }}
    }}
  }}
}});

const tareas  = {str([f"{t[1]['task'][:25]} · {t[1]['project'][:15]}" for t in tasks_sorted])};
const hrsT    = {str([round(t[1]["hours"], 1) for t in tasks_sorted])};
const esInt   = [{', '.join([str(t[1]["internal"]).lower() for t in tasks_sorted])}];
const tarCol  = hrsT.map((_,i) => esInt[i] ? orange : blue);

new Chart(document.getElementById('chartTareas'), {{
  type:'bar',
  data:{{ labels:tareas, datasets:[{{ data:hrsT, backgroundColor:tarCol, borderRadius:{{topRight:3,bottomRight:3}}, borderSkipped:'left' }}] }},
  options:{{
    indexAxis:'y', responsive:true, maintainAspectRatio:false,
    plugins:{{ legend:{{display:false}}, tooltip:{{callbacks:{{label: ctx=>' '+ctx.raw+' hrs'}}}} }},
    scales:{{
      x:{{ grid:{{color:gridC}}, ticks:{{color:muted,font:{{size:11}},callback:v=>v+' h'}}, border:{{color:gridC}} }},
      y:{{ grid:{{display:false}}, ticks:{{color:muted,font:{{size:11}}}}, border:{{color:gridC}} }}
    }}
  }}
}});
</script>
</body>
</html>'''

    return html

def main():
    year = int(sys.argv[1]) if len(sys.argv) > 1 else 2025
    base = '/var/www/bloom-time-report'

    time_file = f'{base}/export.csv' if year == 2025 else f'{base}/{year}.csv'
    records = parse_time_csv(time_file, None)
    ingresos = parse_ingresos_csv(f'{base}/ingresos-{year}.csv', year)
    max_month = 12 if year == 2025 else 6
    metrics = calculate_metrics(records, ingresos, year, max_month)
    html = generate_html(metrics)

    outpath = f'{base}/{year}.html'
    with open(outpath, 'w', encoding='utf-8') as f:
        f.write(html)

    print(f"Informe generado: {year}.html")
    print(f"Total registros {year}: {len(records)}")
    print(f"Total horas: {metrics['total']:.1f}")
    print(f"Billable: {metrics['billable_pct']:.1f}%")
    print(f"Internas: {metrics['internal']:.1f} ({metrics['internal_pct']:.1f}%)")
    print(f"Externas: {metrics['external']:.1f} ({metrics['external_pct']:.1f}%)")
    print(f"Ingresos: {format_clp(metrics['total_ingresos'])}")
    print(f"Cobrado: {format_clp(metrics['total_ingresos_pagados'])}")
    print(f"Pendiente: {format_clp(metrics['total_ingresos_pendientes'])}")

if __name__ == '__main__':
    main()
