/* App shell — sidebar, tabs, estado, persistencia, CRUD, tweaks */
const { useState, useEffect, useRef } = React;

const STORE_KEY = "cl_events_v2";

/* ============================================================
   Importación desde Excel
   Hoja "Solicitudes": Código | Nombre | Color (opcional)
   Hoja "Marcas":      Recurso | Tipo | Código Solicitud | Estado | Fecha Inicio | Fecha Fin
   ============================================================ */
function _normTxt(s) { return String(s == null ? "" : s).trim().toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, ""); }

function _toISOValue(v) {
  if (v == null || v === "") return "";
  if (v instanceof Date && !isNaN(v.getTime())) return window.CL.iso(v);
  if (typeof v === "number" && typeof XLSX !== "undefined" && XLSX.SSF) {
    const d = XLSX.SSF.parse_date_code(v);
    if (d) return d.y + "-" + String(d.m).padStart(2, "0") + "-" + String(d.d).padStart(2, "0");
  }
  const s = String(v).trim();
  if (/^\d{4}-\d{1,2}-\d{1,2}$/.test(s)) {
    const [y, m, d] = s.split("-");
    return y + "-" + m.padStart(2, "0") + "-" + d.padStart(2, "0");
  }
  const m = s.match(/^(\d{1,2})[\/\.\-](\d{1,2})[\/\.\-](\d{2,4})$/);
  if (m) {
    let y = m[3]; if (y.length === 2) y = (+y > 50 ? "19" : "20") + y;
    return y + "-" + m[2].padStart(2, "0") + "-" + m[1].padStart(2, "0");
  }
  const dt = new Date(s);
  if (!isNaN(dt.getTime())) return window.CL.iso(dt);
  return "";
}

function importFromWorkbook(wb) {
  const CL = window.CL;
  const newProjects = [];
  const addedMarcas = [];
  const errors = [];
  const findSheet = (re) => wb.SheetNames.find((n) => re.test(_normTxt(n)));

  // ----- Hoja Solicitudes -----
  const existingIds = new Set(CL.PROJECTS.map((p) => p.id));
  const solSheet = findSheet(/solicit|proyect/);
  if (solSheet) {
    const rows = XLSX.utils.sheet_to_json(wb.Sheets[solSheet], { header: 1, defval: "", raw: true });
    let h = null;
    rows.forEach((row) => {
      if (!Array.isArray(row) || row.every((c) => c === "" || c == null)) return;
      if (!h) {
        const low = row.map(_normTxt);
        const cod = low.findIndex((c) => c.includes("cod"));
        const nom = low.findIndex((c) => c.includes("nom"));
        const col = low.findIndex((c) => c.includes("color"));
        if (cod >= 0 && nom >= 0) h = { cod, nom, col };
        return;
      }
      const cod = String(row[h.cod] || "").trim().toUpperCase();
      const nom = String(row[h.nom] || "").trim();
      const colHex = h.col >= 0 ? String(row[h.col] || "").trim() : "";
      if (!cod || !nom) return;
      if (existingIds.has(cod)) return;
      const used = new Set(CL.PROJECTS.map((p) => p.color).concat(newProjects.map((p) => p.color)));
      const color = /^#[0-9A-Fa-f]{6}$/.test(colHex) ? colHex : (CL.PROJECT_COLORS.find((c) => !used.has(c)) || CL.PROJECT_COLORS[(existingIds.size + newProjects.length) % CL.PROJECT_COLORS.length]);
      newProjects.push({ id: cod, name: nom, color });
      existingIds.add(cod);
    });
  }

  // ----- Hoja Marcas -----
  const mkSheet = findSheet(/marc|asign|carga/);
  if (!mkSheet) {
    errors.push("No se encontró la hoja 'Marcas' en el archivo.");
    return { newProjects, addedMarcas, skipped: 0, errors };
  }
  const resourceByName = new Map();
  CL.ALL_RESOURCES.forEach((r) => resourceByName.set(_normTxt(r.name), r));
  const projById = new Map(CL.PROJECTS.concat(newProjects).map((p) => [p.id, p]));
  const TYPE = { "asignacion": "asignacion", "asignar": "asignacion", "proyecto": "asignacion", "vacaciones": "vacaciones", "vacacion": "vacaciones", "licencia": "licencia", "permiso": "licencia", "capacitacion": "capacitacion", "capac": "capacitacion" };
  const EST = { "en curso": "en_curso", "en_curso": "en_curso", "curso": "en_curso", "completada": "completada", "completado": "completada", "a tiempo": "completada", "en riesgo": "en_riesgo", "en_riesgo": "en_riesgo", "riesgo": "en_riesgo", "retrasada": "retrasada", "retrasado": "retrasada", "retraso": "retrasada" };
  function lookupKey(map, raw) {
    const n = _normTxt(raw); if (!n) return null;
    if (map[n]) return map[n];
    for (const k of Object.keys(map)) if (n.includes(k)) return map[k];
    return null;
  }

  const rows = XLSX.utils.sheet_to_json(wb.Sheets[mkSheet], { header: 1, defval: "", raw: true });
  let h = null, skipped = 0;
  rows.forEach((row, i) => {
    if (!Array.isArray(row) || row.every((c) => c === "" || c == null)) return;
    if (!h) {
      const low = row.map(_normTxt);
      const idxOf = (...keys) => { for (const k of keys) { const j = low.findIndex((c) => c.includes(k)); if (j >= 0) return j; } return -1; };
      const r = idxOf("recurso", "nombre"), t = idxOf("tipo"), c = idxOf("codigo", "solicitud"), e = idxOf("estado"), s = idxOf("inicio"), f = idxOf("fin");
      if (r >= 0 && t >= 0 && s >= 0 && f >= 0) h = { r, t, c, e, s, f };
      return;
    }
    const rowNum = i + 1;
    const rname = String(row[h.r] || "").trim();
    if (!rname) return;
    const res = resourceByName.get(_normTxt(rname));
    if (!res) { errors.push("Fila " + rowNum + ": recurso \"" + rname + "\" no existe."); skipped++; return; }
    const type = lookupKey(TYPE, row[h.t]);
    if (!type) { errors.push("Fila " + rowNum + ": tipo \"" + row[h.t] + "\" no reconocido."); skipped++; return; }
    const start = _toISOValue(row[h.s]);
    const end = _toISOValue(row[h.f]);
    if (!start || !end) { errors.push("Fila " + rowNum + ": fechas inválidas."); skipped++; return; }
    if (CL.parseISO(end).getTime() < CL.parseISO(start).getTime()) { errors.push("Fila " + rowNum + ": fecha fin anterior al inicio."); skipped++; return; }
    const y0 = CL.parseISO(start).getFullYear(), y1 = CL.parseISO(end).getFullYear();
    if (y0 < CL.MIN_YEAR || y1 > CL.MAX_YEAR) { errors.push("Fila " + rowNum + ": año fuera de rango (" + CL.MIN_YEAR + "–" + CL.MAX_YEAR + ")."); skipped++; return; }
    const ev = { id: CL.uid("X") + "_" + i, resourceId: res.id, type, start, end };
    if (type === "asignacion") {
      const code = String(row[h.c] || "").trim().toUpperCase();
      if (!code) { errors.push("Fila " + rowNum + ": asignación sin código de solicitud."); skipped++; return; }
      let proj = projById.get(code);
      if (!proj) {
        const used = new Set(CL.PROJECTS.map((p) => p.color).concat(newProjects.map((p) => p.color)));
        const color = CL.PROJECT_COLORS.find((c) => !used.has(c)) || CL.PROJECT_COLORS[(CL.PROJECTS.length + newProjects.length) % CL.PROJECT_COLORS.length];
        proj = { id: code, name: code, color };
        newProjects.push(proj); projById.set(code, proj);
      }
      ev.projectId = code; ev.title = proj.name;
      const estado = lookupKey(EST, row[h.e]) || "en_curso";
      ev.estado = estado; ev.planEnd = end;
      if (estado === "completada" || estado === "retrasada") { ev.realEnd = end; ev.onTime = estado === "completada"; }
      else { ev.realEnd = null; ev.onTime = null; }
    } else {
      ev.title = CL.EVENT_TYPES[type].label;
    }
    addedMarcas.push(ev);
  });
  if (!h) errors.push("La hoja '" + mkSheet + "' no tiene la cabecera esperada (Recurso, Tipo, Código Solicitud, Estado, Fecha Inicio, Fecha Fin).");
  return { newProjects, addedMarcas, skipped, errors };
}

// Cargar configuración (equipos / solicitudes) antes de render
window.CL.loadConfig();

function loadEvents() {
  let evs;
  try {
    const raw = localStorage.getItem(STORE_KEY);
    evs = raw ? JSON.parse(raw) : window.CL.EVENTS.map((e) => Object.assign({}, e));
  } catch (e) { evs = window.CL.EVENTS.map((e) => Object.assign({}, e)); }
  // descartar marcas de recursos inexistentes, y asignaciones cuya solicitud ya no existe
  return evs.filter((e) => {
    if (!window.CL.RESOURCE_MAP.has(e.resourceId)) return false;
    if (e.type === "asignacion" && e.projectId && !window.CL.PROJECT_MAP.has(e.projectId)) return false;
    return true;
  });
}
function saveEvents(evs) { try { localStorage.setItem(STORE_KEY, JSON.stringify(evs)); } catch (e) {} }

function Tooltip({ tip }) {
  if (!tip) return null;
  return <div className="tip" style={{ left: tip.x, top: tip.y }} dangerouslySetInnerHTML={{ __html: tip.html }}></div>;
}

const THEMES = {
  balanceado: { a: "#E11D2A", a2: "#7C3AED", soft: "#F6E9F2" },
  rojo: { a: "#E11D2A", a2: "#B8154A", soft: "#FCE7E9" },
  morado: { a: "#7C3AED", a2: "#5B21B6", soft: "#EDE9FE" },
};

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "tema": "balanceado",
  "vivo": true
}/*EDITMODE-END*/;

function defaultScope() {
  const CL = window.CL;
  if (CL.RESOURCE_MAP.has("PM1-R1")) return { type: "resource", id: "PM1-R1" };
  if (CL.ALL_RESOURCES.length) return { type: "resource", id: CL.ALL_RESOURCES[0].id };
  if (CL.TEAMS.length) return { type: "team", id: CL.TEAMS[0].id };
  return { type: "all" };
}

function Sidebar({ scope, setScope, openTeams, toggleTeam, year, onNewPM, onEditPM, onDeletePM, onNewResource, onEditResource, onDeleteResource }) {
  const CL = window.CL;
  return (
    <div className="sidebar">
      <div className="brand">
        <div className="brand-mark">
          <svg viewBox="0 0 24 24" fill="none"><path d="M4 13l4 4 12-12" stroke="#fff" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"/><path d="M4 19h16" stroke="#fff" strokeWidth="2.4" strokeLinecap="round" opacity=".55"/></svg>
        </div>
        <div>
          <div className="brand-title">Carga Laboral</div>
          <div className="brand-sub">Lima, Perú · {year}</div>
        </div>
      </div>
      <div className="side-scroll">
        <button className={"allbtn" + (scope.type === "all" ? " active" : "")} onClick={() => setScope({ type: "all" })}>
          {Icon.users({ width: 17, height: 17 })} Todos los equipos
          <span style={{ marginLeft: "auto", fontSize: 11, opacity: .7 }}>{CL.ALL_RESOURCES.length}</span>
        </button>

        <div className="section-row">
          <div className="side-section-label">Jefes de Proyecto</div>
          <button className="side-add" title="Nuevo Jefe de Proyecto" onClick={onNewPM}>{Icon.plus({})}</button>
        </div>

        {CL.TEAMS.map((t) => {
          const open = openTeams.has(t.id);
          return (
            <div className="team" key={t.id}>
              <div className="row-wrap">
                <button className={"team-head" + (open ? " open" : "") + (scope.type === "team" && scope.id === t.id ? " active" : "")}
                  onClick={() => { setScope({ type: "team", id: t.id }); if (!open) toggleTeam(t.id); }} style={{ width: "100%" }}>
                  <span className="team-dot" style={{ background: avatarColor(t.id) }}></span>
                  <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{t.pm}</span>
                  <span className="team-meta">{t.team}</span>
                  <span className="chev" onClick={(e) => { e.stopPropagation(); toggleTeam(t.id); }}>{Icon.chevR({ width: 15, height: 15 })}</span>
                </button>
                <div className="side-actions">
                  <button className="icon-act" title="Editar equipo" onClick={(e) => { e.stopPropagation(); onEditPM(t); }}>{Icon.pencil({})}</button>
                  <button className="icon-act danger" title="Eliminar equipo" onClick={(e) => { e.stopPropagation(); onDeletePM(t); }}>{Icon.trash({})}</button>
                </div>
              </div>
              {open && (
                <div className="res-list">
                  {t.resources.map((r) => (
                    <div className="row-wrap" key={r.id}>
                      <button className={"res" + (scope.type === "resource" && scope.id === r.id ? " active" : "")}
                        onClick={() => setScope({ type: "resource", id: r.id })} style={{ width: "100%" }}>
                        <span className="avatar" style={{ background: avatarColor(r.id) }}>{r.initials}</span>
                        <span className="res-name">{r.name}</span>
                        <span className="res-role">{r.roleShort}</span>
                      </button>
                      <div className="side-actions">
                        <button className="icon-act" title="Editar recurso" onClick={(e) => { e.stopPropagation(); onEditResource(r, t); }}>{Icon.pencil({})}</button>
                        <button className="icon-act danger" title="Eliminar recurso" onClick={(e) => { e.stopPropagation(); onDeleteResource(r, t); }}>{Icon.trash({})}</button>
                      </div>
                    </div>
                  ))}
                  <button className="add-res-btn" onClick={() => onNewResource(t)}>{Icon.plus({})} Agregar recurso</button>
                </div>
              )}
            </div>
          );
        })}
      </div>
    </div>
  );
}

function App() {
  const CL = window.CL;
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [events, setEvents] = useState(loadEvents);
  const [scope, setScope] = useState(defaultScope);
  const [tab, setTab] = useState("calendario");
  const [year, setYear] = useState(CL.YEAR);
  const [month, setMonth] = useState(0);
  const [openTeams, setOpenTeams] = useState(new Set([CL.TEAMS[0] ? CL.TEAMS[0].id : ""]));
  const [modal, setModal] = useState(null);
  const [tip, setTip] = useState(null);
  const [rev, setRev] = useState(0);
  const bump = () => setRev((r) => r + 1);
  const [menuOpen, setMenuOpen] = useState(false);
  const fileRef = useRef(null);
  const excelRef = useRef(null);
  const [toast, setToast] = useState(null);
  const toastRef = useRef(null);
  function showToast(msg) { setToast(msg); if (toastRef.current) clearTimeout(toastRef.current); toastRef.current = setTimeout(() => setToast(null), 2200); }

  useEffect(() => {
    if (!menuOpen) return;
    const h = (e) => { if (!e.target.closest(".data-menu-wrap")) setMenuOpen(false); };
    document.addEventListener("mousedown", h);
    return () => document.removeEventListener("mousedown", h);
  }, [menuOpen]);

  useEffect(() => { saveEvents(events); }, [events]);
  useEffect(() => {
    const th = THEMES[t.tema] || THEMES.balanceado;
    const root = document.documentElement.style;
    root.setProperty("--accent", th.a); root.setProperty("--accent-2", th.a2); root.setProperty("--accent-soft", th.soft);
    document.body.classList.toggle("flat", !t.vivo);
  }, [t.tema, t.vivo]);
  // validar scope si se eliminó la entidad
  useEffect(() => {
    if (scope.type === "resource" && !CL.RESOURCE_MAP.has(scope.id)) setScope({ type: "all" });
    if (scope.type === "team" && !CL.TEAMS.find((x) => x.id === scope.id)) setScope({ type: "all" });
  }, [rev]);

  function toggleTeam(id) { setOpenTeams((p) => { const n = new Set(p); n.has(id) ? n.delete(id) : n.add(id); return n; }); }
  const close = () => setModal(null);

  // navegación de calendario (mes rueda entre años)
  const canPrev = year > CL.MIN_YEAR || month > 0;
  const canNext = year < CL.MAX_YEAR || month < 11;
  function prevMonth() { if (month === 0) { if (year > CL.MIN_YEAR) { setYear(year - 1); setMonth(11); } } else setMonth(month - 1); }
  function nextMonth() { if (month === 11) { if (year < CL.MAX_YEAR) { setYear(year + 1); setMonth(0); } } else setMonth(month + 1); }
  function changeYear(y) { const ny = Math.max(CL.MIN_YEAR, Math.min(CL.MAX_YEAR, y)); setYear(ny); }

  const resource = scope.type === "resource" ? CL.RESOURCE_MAP.get(scope.id) : null;
  const resourceIds = scope.type === "all" ? CL.ALL_RESOURCES.map((r) => r.id)
    : scope.type === "team" ? (CL.TEAMS.find((x) => x.id === scope.id)?.resources || []).map((r) => r.id)
    : [scope.id];

  const curTeam = scope.type === "team" ? CL.TEAMS.find((x) => x.id === scope.id) : (resource ? CL.TEAMS.find((x) => x.id === resource.pmId) : null);
  const scopeName = scope.type === "all" ? "Todos los equipos"
    : scope.type === "team" ? "Equipo " + curTeam?.team + " · " + curTeam?.pm
    : resource ? resource.name : "—";
  const scopeSub = scope.type === "all" ? CL.ALL_RESOURCES.length + " recursos · " + CL.TEAMS.length + " jefes de proyecto"
    : scope.type === "team" ? curTeam?.resources.length + " recursos · " + curTeam?.pm
    : resource ? resource.role + " · Equipo " + curTeam?.team : "";

  // ----- Eventos (marcas) -----
  function openAdd() {
    const start = CL.iso(CL.nextWorkingFrom(new Date(year, month, 1)));
    const end = CL.endAfterWorkingDays(start, 5);
    setModal({ kind: "event", initial: { type: "asignacion", start, end } });
  }
  function openEdit(ev) { setModal({ kind: "event", initial: ev }); }
  function saveEvent(ev) {
    setEvents((prev) => ev.id ? prev.map((e) => (e.id === ev.id ? ev : e)) : prev.concat([Object.assign({}, ev, { id: "U" + Date.now() })]));
    showToast(ev.id ? "Marca guardada" : "Marca agregada");
    close();
  }
  function deleteEvent(id) { setEvents((prev) => prev.filter((e) => e.id !== id)); showToast("Marca eliminada"); close(); }

  // ----- CRUD Jefe de Proyecto -----
  function savePM(d) {
    if (d.id) { const tm = CL.TEAMS.find((x) => x.id === d.id); if (tm) { tm.pm = d.pm; tm.team = d.team; } }
    else {
      const id = CL.uid("PM");
      const team = { id, pm: d.pm, team: d.team, resources: [] };
      if (d.template) {
        const plan = [["Desarrollador", "Dev", "Desarrollador 1"], ["Desarrollador", "Dev", "Desarrollador 2"], ["Desarrollador", "Dev", "Desarrollador 3"], ["Analista QA", "QA", "Analista QA"], ["Analista Prod.", "Prod", "Analista Producción"]];
        plan.forEach((p) => team.resources.push({ id: CL.uid(id + "-R"), name: p[2], initials: CL.initialsOf(p[2]), role: p[0], roleShort: p[1], pmId: id }));
      }
      CL.TEAMS.push(team);
      setOpenTeams((prev) => new Set(prev).add(id));
      setScope({ type: "team", id });
    }
    CL.rebuildIndexes(); CL.saveConfig(); bump(); close();
    showToast(d.id ? "Jefe de Proyecto guardado" : "Equipo creado");
  }
  function deletePM(team) {
    if (!window.confirm(`¿Eliminar el equipo "${team.team}" (${team.pm}) y sus ${team.resources.length} recursos? También se borrarán sus marcas.`)) return;
    const ids = new Set(team.resources.map((r) => r.id));
    CL.TEAMS = CL.TEAMS.filter((x) => x.id !== team.id);
    setEvents((prev) => prev.filter((e) => !ids.has(e.resourceId)));
    if ((scope.type === "team" && scope.id === team.id) || (scope.type === "resource" && ids.has(scope.id))) setScope({ type: "all" });
    CL.rebuildIndexes(); CL.saveConfig(); bump(); close();
    showToast("Equipo eliminado");
  }

  // ----- CRUD Recurso -----
  function saveResource(d, teamId) {
    const tm = CL.TEAMS.find((x) => x.id === teamId); if (!tm) return;
    const rp = CL.ROLES.find((r) => r.role === d.role) || CL.ROLES[0];
    if (d.id) { const r = tm.resources.find((x) => x.id === d.id); if (r) { r.name = d.name; r.role = d.role; r.roleShort = rp.roleShort; r.initials = CL.initialsOf(d.name); } }
    else {
      const id = CL.uid(teamId + "-R");
      tm.resources.push({ id, name: d.name, initials: CL.initialsOf(d.name), role: d.role, roleShort: rp.roleShort, pmId: teamId });
      setScope({ type: "resource", id });
      setOpenTeams((prev) => new Set(prev).add(teamId));
    }
    CL.rebuildIndexes(); CL.saveConfig(); bump(); close();
    showToast(d.id ? "Recurso guardado" : "Recurso agregado");
  }
  function deleteResource(res, team) {
    if (!window.confirm(`¿Eliminar a "${res.name}" del equipo ${team.team}? También se borrarán sus marcas.`)) return;
    const tm = CL.TEAMS.find((x) => x.id === team.id); if (!tm) return;
    tm.resources = tm.resources.filter((r) => r.id !== res.id);
    setEvents((prev) => prev.filter((e) => e.resourceId !== res.id));
    if (scope.type === "resource" && scope.id === res.id) setScope({ type: "team", id: team.id });
    CL.rebuildIndexes(); CL.saveConfig(); bump(); close();
    showToast("Recurso eliminado");
  }

  // ----- CRUD Solicitudes -----
  function saveProjects({ newProjects, remap, removed }) {
    CL.PROJECTS = newProjects;
    const removedSet = new Set(removed);
    setEvents((prev) => prev
      .filter((e) => !(e.type === "asignacion" && removedSet.has(e.projectId)))
      .map((e) => {
        if (e.type !== "asignacion") return e;
        let pid = e.projectId;
        if (remap[pid]) pid = remap[pid];
        const proj = newProjects.find((p) => p.id === pid);
        return Object.assign({}, e, { projectId: pid, title: proj ? proj.name : e.title });
      }));
    CL.rebuildIndexes(); CL.saveConfig(); bump(); close();
    showToast("Solicitudes actualizadas");
  }

  function pickResource(id) { setScope({ type: "resource", id }); const r = CL.RESOURCE_MAP.get(id); if (r) setOpenTeams((p) => new Set(p).add(r.pmId)); setTab("calendario"); }

  // ----- Exportar / Importar / Restablecer -----
  function downloadBlob(blob, name) {
    const url = URL.createObjectURL(blob); const a = document.createElement("a");
    a.href = url; a.download = name; document.body.appendChild(a); a.click(); a.remove();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  }
  function exportExcel() {
    setMenuOpen(false);
    if (typeof XLSX === "undefined") { exportCsvFallback(); return; }
    const wb = XLSX.utils.book_new();
    const resRows = [["Equipo", "Jefe de Proyecto", "Recurso", "Rol"]];
    CL.TEAMS.forEach((tm) => tm.resources.forEach((r) => resRows.push([tm.team, tm.pm, r.name, r.role])));
    XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(resRows), "Recursos");
    const mk = [["Recurso", "Equipo", "Tipo", "Solicitud", "Detalle", "Inicio", "Fin", "Estado", "Días laborables"]];
    events.slice().sort((a, b) => (a.start < b.start ? -1 : 1)).forEach((e) => {
      const r = CL.RESOURCE_MAP.get(e.resourceId); const tm = r ? CL.TEAMS.find((x) => x.id === r.pmId) : null;
      mk.push([r ? r.name : e.resourceId, tm ? tm.team : "", CL.EVENT_TYPES[e.type].label, e.projectId || "", e.title, e.start, e.end, e.estado ? estadoLabel(e.estado) : "", CL.workingDaysBetween(e.start, e.end)]);
    });
    XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(mk), "Marcas");
    const sol = [["Código", "Nombre"]]; CL.PROJECTS.forEach((p) => sol.push([p.id, p.name]));
    XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(sol), "Solicitudes");
    const kp = [["Equipo", "Jefe de Proyecto", "% Cumplimiento", "% Ocupación", "Solicitudes", "En riesgo", "Prom. días/solicitud"]];
    CL.TEAMS.forEach((tm) => { const k = CLA.computeKpis(events, { type: "team", id: tm.id }); kp.push([tm.team, tm.pm, k.cumplimiento, k.ocupacion, k.atendidas, k.enRiesgo, k.promDias]); });
    const kg = CLA.computeKpis(events, { type: "all" }); kp.push(["TODOS", "", kg.cumplimiento, kg.ocupacion, kg.atendidas, kg.enRiesgo, kg.promDias]);
    XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(kp), "KPIs");
    XLSX.writeFile(wb, "carga-laboral-" + CL.YEAR + ".xlsx");
  }
  function exportCsvFallback() {
    const rows = [["Recurso", "Equipo", "Tipo", "Solicitud", "Detalle", "Inicio", "Fin", "Estado", "Días laborables"]];
    events.forEach((e) => { const r = CL.RESOURCE_MAP.get(e.resourceId); const tm = r ? CL.TEAMS.find((x) => x.id === r.pmId) : null; rows.push([r ? r.name : e.resourceId, tm ? tm.team : "", CL.EVENT_TYPES[e.type].label, e.projectId || "", e.title, e.start, e.end, e.estado ? estadoLabel(e.estado) : "", CL.workingDaysBetween(e.start, e.end)]); });
    const csv = rows.map((r) => r.map((c) => '"' + String(c).replace(/"/g, '""') + '"').join(",")).join("\n");
    downloadBlob(new Blob(["\ufeff" + csv], { type: "text/csv;charset=utf-8" }), "carga-laboral.csv");
  }
  function exportConfig() {
    setMenuOpen(false);
    const data = { app: "carga-laboral", version: 1, year: CL.YEAR, teams: CL.TEAMS, projects: CL.PROJECTS, events };
    downloadBlob(new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }), "carga-laboral-respaldo.json");
  }
  function triggerImport() { setMenuOpen(false); if (fileRef.current) fileRef.current.click(); }
  function onImportFile(e) {
    const f = e.target.files && e.target.files[0]; if (!f) return;
    const reader = new FileReader();
    reader.onload = () => {
      try {
        const d = JSON.parse(reader.result);
        if (!Array.isArray(d.teams) || !Array.isArray(d.projects)) throw 0;
        CL.TEAMS = d.teams; CL.PROJECTS = d.projects; CL.rebuildIndexes(); CL.saveConfig();
        setEvents(Array.isArray(d.events) ? d.events.filter((ev) => CL.RESOURCE_MAP.has(ev.resourceId)) : []);
        setScope(defaultScope()); setOpenTeams(new Set([CL.TEAMS[0] ? CL.TEAMS[0].id : ""])); bump();
        showToast("Respaldo importado");
      } catch (err) { alert("No se pudo leer el archivo. Debe ser un respaldo .json válido de Carga Laboral."); }
    };
    reader.readAsText(f); e.target.value = "";
  }
  function resetData() {
    setMenuOpen(false);
    if (!window.confirm("¿Restablecer a los datos de ejemplo? Se perderán todos los cambios, equipos, recursos y marcas actuales.")) return;
    localStorage.removeItem("cl_config_v2"); localStorage.removeItem(STORE_KEY); location.reload();
  }

  // ----- Importar desde Excel (solicitudes + marcas por nombre de recurso) -----
  function triggerExcel() { setMenuOpen(false); if (excelRef.current) excelRef.current.click(); }
  function downloadTemplate() {
    setMenuOpen(false);
    if (typeof XLSX === "undefined") { alert("La librería de Excel no se cargó. Revisa tu conexión a internet y recarga la página."); return; }
    const wb = XLSX.utils.book_new();
    const sol = [
      ["Código", "Nombre", "Color (opcional)"],
      ["SOL-3001", "Nueva integración API", "#7C3AED"],
      ["SOL-3002", "Reporte regulatorio Q3", "#E11D2A"],
    ];
    XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(sol), "Solicitudes");
    const ej = CL.ALL_RESOURCES[0]?.name || "Mateo Quispe";
    const ej2 = CL.ALL_RESOURCES[1]?.name || "Valentina Mamani";
    const mk = [
      ["Recurso", "Tipo", "Código Solicitud", "Estado", "Fecha Inicio", "Fecha Fin"],
      [ej, "asignacion", "SOL-3001", "en_curso", "2026-07-06", "2026-07-31"],
      [ej, "vacaciones", "", "", "2026-08-03", "2026-08-14"],
      [ej2, "capacitacion", "", "", "2026-09-07", "2026-09-09"],
      [ej2, "asignacion", "SOL-3002", "en_riesgo", "2026-10-05", "2026-10-30"],
    ];
    XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(mk), "Marcas");
    const inst = [
      ["INSTRUCCIONES DE CARGA"],
      [""],
      ["Hoja 'Solicitudes': código + nombre. Color opcional en formato #RRGGBB."],
      ["Hoja 'Marcas': una fila por marca. Se busca el recurso por nombre exacto"],
      ["(no distingue mayúsculas ni tildes)."],
      [""],
      ["Tipo aceptado: asignacion | vacaciones | licencia | capacitacion"],
      ["Estado (solo asignación): en_curso | completada | en_riesgo | retrasada"],
      ["Código Solicitud: requerido para tipo asignación. Si no existe en la app,"],
      ["se crea automáticamente con el mismo código como nombre."],
      ["Fechas: formatos YYYY-MM-DD, DD/MM/YYYY o fecha de Excel."],
      ["Rango de años permitido: " + CL.MIN_YEAR + "–" + CL.MAX_YEAR + "."],
    ];
    XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(inst), "Instrucciones");
    XLSX.writeFile(wb, "carga-laboral-plantilla.xlsx");
    showToast("Plantilla descargada");
  }
  function onExcelFile(e) {
    const f = e.target.files && e.target.files[0]; if (!f) { return; }
    const reader = new FileReader();
    reader.onload = () => {
      try {
        if (typeof XLSX === "undefined") throw new Error("La librería Excel no está cargada.");
        const wb = XLSX.read(reader.result, { type: "array", cellDates: true });
        const res = importFromWorkbook(wb);
        if (res.newProjects.length) { CL.PROJECTS = CL.PROJECTS.concat(res.newProjects); CL.rebuildIndexes(); CL.saveConfig(); }
        if (res.addedMarcas.length) setEvents((prev) => prev.concat(res.addedMarcas));
        bump();
        const partes = [];
        if (res.addedMarcas.length) partes.push(res.addedMarcas.length + " marca" + (res.addedMarcas.length === 1 ? "" : "s"));
        if (res.newProjects.length) partes.push(res.newProjects.length + " solicitud" + (res.newProjects.length === 1 ? "" : "es"));
        if (res.skipped) partes.push(res.skipped + " omitida" + (res.skipped === 1 ? "" : "s"));
        showToast(partes.length ? "Importado: " + partes.join(" · ") : "Sin cambios desde el archivo");
        if (res.errors.length) {
          const head = "Importación finalizada con observaciones:\n\n";
          const list = res.errors.slice(0, 15).join("\n");
          const tail = res.errors.length > 15 ? "\n…(" + (res.errors.length - 15) + " observación(es) más)" : "";
          alert(head + list + tail);
        }
      } catch (err) {
        alert("No se pudo leer el archivo Excel: " + (err && err.message ? err.message : "formato no válido"));
      }
    };
    reader.readAsArrayBuffer(f); e.target.value = "";
  }

  const tabs = [
    { k: "calendario", label: "Calendario", icon: Icon.cal },
    { k: "anual", label: "Resumen anual", icon: Icon.grid },
    { k: "kpis", label: "KPIs", icon: Icon.chart },
  ];

  return (
    <div className="app">
      <Sidebar scope={scope} setScope={setScope} openTeams={openTeams} toggleTeam={toggleTeam} year={year}
        onNewPM={() => setModal({ kind: "pm", initial: null })}
        onEditPM={(tm) => setModal({ kind: "pm", initial: tm })}
        onDeletePM={deletePM}
        onNewResource={(tm) => setModal({ kind: "resource", initial: null, teamId: tm.id, teamName: tm.team })}
        onEditResource={(r, tm) => setModal({ kind: "resource", initial: r, teamId: tm.id, teamName: tm.team })}
        onDeleteResource={deleteResource} />

      <div className="main">
        <div className="topbar">
          <div className="crumb">
            <h1>{scopeName}</h1>
            <div className="sub">{scopeSub}</div>
          </div>
          <div className="tabs">
            {tabs.map((tb) => (
              <button key={tb.k} className={"tab" + (tab === tb.k ? " active" : "")} onClick={() => setTab(tb.k)}>{tb.icon({})}{tb.label}</button>
            ))}
          </div>
          <div className="spacer"></div>
          <button className="btn btn-ghost" onClick={() => setModal({ kind: "projects" })} title="Gestionar códigos de solicitud">{Icon.doc({ width: 15, height: 15 })} Solicitudes</button>
          <div className="data-menu-wrap">
            <button className="btn btn-ghost" onClick={() => setMenuOpen((o) => !o)} title="Exportar, importar y restablecer">{Icon.download({ width: 15, height: 15 })} Datos {Icon.chevDown({ width: 13, height: 13 })}</button>
            {menuOpen && (
              <div className="data-menu">
                <div className="menu-cap">Importar</div>
                <button className="menu-item" onClick={triggerExcel}>{Icon.upload({})}<span>Importar desde Excel<div className="mi-sub">Solicitudes y marcas por recurso</div></span></button>
                <button className="menu-item" onClick={downloadTemplate}>{Icon.sheet({})}<span>Descargar plantilla Excel<div className="mi-sub">Formato esperado con ejemplos</div></span></button>
                <button className="menu-item" onClick={triggerImport}>{Icon.download({})}<span>Importar respaldo (.json)<div className="mi-sub">Cargar un .json exportado</div></span></button>
                <div className="menu-sep"></div>
                <div className="menu-cap">Exportar</div>
                <button className="menu-item" onClick={exportExcel}>{Icon.sheet({})}<span>Exportar a Excel<div className="mi-sub">Recursos, marcas y KPIs</div></span></button>
                <button className="menu-item" onClick={exportConfig}>{Icon.download({})}<span>Exportar respaldo<div className="mi-sub">Archivo .json para restaurar</div></span></button>
                <div className="menu-sep"></div>
                <button className="menu-item danger" onClick={resetData}>{Icon.refresh({})}<span>Restablecer datos de ejemplo<div className="mi-sub">Vuelve al estado inicial</div></span></button>
              </div>
            )}
            <input ref={fileRef} type="file" accept="application/json,.json" style={{ display: "none" }} onChange={onImportFile} />
            <input ref={excelRef} type="file" accept=".xlsx,.xls,.csv" style={{ display: "none" }} onChange={onExcelFile} />
          </div>
          <div className="year-select">
            <button className="navbtn" onClick={() => changeYear(year - 1)} disabled={year <= CL.MIN_YEAR} title="Año anterior">{Icon.chevL({})}</button>
            <span className="ys-val">{Icon.cal({ width: 14, height: 14 })}{year}</span>
            <button className="navbtn" onClick={() => changeYear(year + 1)} disabled={year >= CL.MAX_YEAR} title="Año siguiente">{Icon.chevR({})}</button>
          </div>
        </div>

        <div className="content">
          {tab === "calendario" && (
            scope.type === "resource" && resource ? (
              <div className="cal-layout">
                <Calendar scope={scope} resource={resource} resourceIds={resourceIds} events={events} year={year} month={month} setMonth={setMonth} onPrev={prevMonth} onNext={nextMonth} canPrev={canPrev} canNext={canNext} onAddOpen={openAdd} onEditOpen={openEdit} setTip={setTip} />
                <ResourceSidePanel resource={resource} events={events} year={year} month={month} />
              </div>
            ) : (
              <Calendar scope={scope} resource={null} resourceIds={resourceIds} events={events} year={year} month={month} setMonth={setMonth} onPrev={prevMonth} onNext={nextMonth} canPrev={canPrev} canNext={canNext} onAddOpen={openAdd} onEditOpen={openEdit} setTip={setTip} />
            )
          )}
          {tab === "anual" && <AnnualHeatmap events={events} scope={scope} year={year} onPickResource={pickResource} setTip={setTip} />}
          {tab === "kpis" && <KpisView events={events} scope={scope} year={year} />}
        </div>
      </div>

      {modal?.kind === "event" && resource && <EventModal initial={modal.initial} resource={resource} onSave={saveEvent} onDelete={deleteEvent} onClose={close} />}
      {modal?.kind === "pm" && <PMModal initial={modal.initial} onSave={savePM} onDelete={(id) => { const tm = CL.TEAMS.find((x) => x.id === id); if (tm) deletePM(tm); }} onClose={close} />}
      {modal?.kind === "resource" && <ResourceModal initial={modal.initial} teamName={modal.teamName} onSave={(d) => saveResource(d, modal.teamId)} onDelete={(id) => { const tm = CL.TEAMS.find((x) => x.id === modal.teamId); const r = tm?.resources.find((x) => x.id === id); if (r && tm) deleteResource(r, tm); }} onClose={close} />}
      {modal?.kind === "projects" && <ProjectsModal events={events} onCommit={saveProjects} onClose={close} />}

      <Tooltip tip={tip} />
      {toast && <div className="toast">{Icon.check({ width: 16, height: 16 })}{toast}</div>}

      <TweaksPanel>
        <TweakSection label="Estilo visual" />
        <TweakRadio label="Acento" value={t.tema} options={["balanceado", "rojo", "morado"]} onChange={(v) => setTweak("tema", v)} />
        <TweakToggle label="Degradados vivos" value={t.vivo} onChange={(v) => setTweak("vivo", v)} />
      </TweaksPanel>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
