import './planner.css';
import { calculatePlan, exportPlanCsv, newTaskId, plannerExamples, PLANNER_LIMITS, validatePlan, type Plan, type PlanTask } from './core';
import { escapeHtml as e, downloadBlob, downloadJson, confirmReplace, loadDraft, saveDraft, clearSavedDraft } from '../shared/ui';
import { friendlyError, InputError, parseJson, type Issue } from '../shared/validation';
import { nextWorkingDay, isDate } from '../shared/dates';
import { dateFromFrappe } from './chart-model';
import type { TimelineView } from './chart';

const copy = <T>(value: T): T => structuredClone(value);
const money = (value: string, currency: string) => new Intl.NumberFormat('en-CA', { style: 'currency', currency, currencyDisplay: 'code' }).format(Number(value));
const storageKey = 'project-planner';
type TaskField = 'title' | 'phase' | 'start' | 'end' | 'hours' | 'rate' | 'progressPercent' | 'predecessors';

export function mount(root: HTMLElement): () => void {
  let plan = copy(plannerExamples['customer-portal']);
  let remembered = false;
  let busy = false;
  let alive = true;
  let renderVersion = 0;
  let clearChart: (() => void) | undefined;
  let view: TimelineView = 'Week';
  let showChart = !window.matchMedia('(max-width: 767px)').matches;
  let notice = 'Start with the sample plan, then replace the illustrative hours, rates and dates with your own.';
  let noticeError = false;
  let pageSize: 'A4' | 'Letter' = 'A4';
  let dirty = false;
  let inputRevision = 0;
  let pendingInput = false;

  const tell = (message: string, error = false) => {
    notice = message; noticeError = error;
    const status = root.querySelector<HTMLElement>('[data-planner-status]');
    if (status) { status.textContent = notice; status.setAttribute('role', error ? 'alert' : 'status'); status.classList.toggle('et-error', error); }
  };
  const persist = () => {
    if (!remembered) return;
    const result = validatePlan(plan);
    if (!result.ok) { tell('Correct the highlighted fields before this draft can be saved. Your previous saved draft is unchanged.', true); return; }
    try { saveDraft(storageKey, result.value); } catch (error) { remembered = false; tell(friendlyError(error), true); }
  };
  const field = (label: string, name: string, value: string, options = '') => `<label class="et-planner-field">${label}<input data-project-field="${name}" data-focus="project-${name}" value="${e(value)}" ${options}></label>`;
  const taskInput = (task: PlanTask, index: number, label: string, name: Exclude<TaskField, 'predecessors'>, options = '') => `<label><span class="et-planner-cell-label">${label}</span><input aria-label="Task ${index + 1} ${label.toLowerCase()}" data-task="${e(task.id)}" data-task-field="${name}" data-focus="${e(task.id)}-${name}" value="${e(task[name])}" ${options}></label>`;

  function render() {
    const active = document.activeElement instanceof HTMLElement && root.contains(document.activeElement) ? document.activeElement.dataset.focus : undefined;
    const activeButton = document.activeElement instanceof HTMLButtonElement && root.contains(document.activeElement) ? { ...document.activeElement.dataset } : undefined;
    clearChart?.(); clearChart = undefined;
    const version = ++renderVersion;
    const checked = validatePlan(plan);
    const result = checked.ok ? calculatePlan(checked.value) : null;
    const errors = checked.ok ? [] : checked.errors;
    const warnings = checked.warnings;
    const span = result?.calendarDays ?? 0;
    const dayTooLong = span > 730;
    const weekTooLong = span > 3650;
    if (dayTooLong && view === 'Day') view = 'Week';
    if (weekTooLong && view !== 'Month') view = 'Month';
    root.innerHTML = `
      <div class="et-planner" aria-label="Project planner workspace">
        <div class="et-planner-toolbar">
          <label class="et-planner-field">Start from an example<select data-template aria-label="Choose a project example"><option value="">Choose an example…</option>${Object.entries(plannerExamples).map(([id, example]) => `<option value="${id}">${e(example.title)}</option>`).join('')}</select></label>
          <button type="button" data-action="import">Import JSON</button>
          <button type="button" data-action="restore">Restore saved draft</button>
          <button type="button" data-action="clear">Clear draft</button>
          <input type="file" data-import accept=".json,application/json" hidden aria-label="Import a saved project plan">
        </div>
        <p class="et-planner-status ${noticeError ? 'et-error' : ''}" data-planner-status role="${noticeError ? 'alert' : 'status'}" aria-live="polite">${e(notice)}</p>
        <section class="et-planner-panel" aria-labelledby="et-plan-details">
          <h2 id="et-plan-details">Your project</h2>
          <div class="et-planner-details">
            ${field('Project name', 'title', plan.title, 'maxlength="160" required')}
            <label class="et-planner-field">Currency<select data-project-field="currency" data-focus="project-currency">${['CAD', 'USD', 'EUR', 'GBP', 'AUD'].map(currency => `<option ${plan.currency === currency ? 'selected' : ''}>${currency}</option>`).join('')}</select></label>
            ${field('Reference date', 'referenceDate', plan.referenceDate, 'type="date" min="1900-01-01" max="2199-12-31" required')}
            ${field('Extra budget for the unexpected (%)', 'contingencyPercent', plan.contingencyPercent, 'type="number" min="0" max="100" step="0.01" inputmode="decimal" required')}
          </div>
          <label class="et-planner-field et-planner-description">What is the project for?<textarea data-project-field="description" data-focus="project-description" maxlength="4000" rows="2">${e(plan.description)}</textarea></label>
          <p class="et-planner-help">The reference date is for your records; changing it does not move tasks. Changing currency changes the label, with no conversion. Sample rates are illustrative, not Essential Designs prices.</p>
        </section>
        <section class="et-planner-summary" aria-label="Estimated project budget" ${!result ? 'aria-busy="true"' : ''}>
          <div><span>Total hours</span><strong data-total-hours>${result ? e(result.totalHours) : '—'}</strong></div>
          <div><span>Base cost</span><strong data-base-cost>${result ? e(money(result.baseCost, plan.currency)) : '—'}</strong></div>
          <div><span>Extra budget (${e(plan.contingencyPercent)}%)</span><strong data-contingency>${result ? e(money(result.contingency, plan.currency)) : '—'}</strong></div>
          <div><span>Total estimated cost</span><strong data-total-cost>${result ? e(money(result.totalCost, plan.currency)) : '—'}</strong></div>
          <p>Planning estimate based on your inputs—not a quotation.</p>
        </section>
        <div data-validation>${issueList(errors, 'Please correct these fields', 'et-error')}${issueList(warnings, 'Schedule conflicts to review', 'et-warning')}</div>
        <section class="et-planner-panel" aria-labelledby="et-plan-tasks">
          <div class="et-planner-heading"><div><h2 id="et-plan-tasks">Tasks and dates</h2><p class="et-planner-help">Edit tasks here or adjust dates on the timeline. Hours are separate from duration. Dependent tasks will not move automatically.</p></div><button type="button" data-action="add" ${plan.tasks.length >= PLANNER_LIMITS.tasks ? 'disabled' : ''}>Add task</button></div>
          <div class="et-planner-table-wrap">
            <table class="et-planner-table"><caption class="et-planner-sr">Editable project tasks. Use the previous task selector for finish-to-start dependencies.</caption><thead><tr><th scope="col">Task / phase</th><th scope="col">Start / end</th><th scope="col">Hours / rate</th><th scope="col">Progress / cost</th><th scope="col">Previous tasks</th><th scope="col">Actions</th></tr></thead><tbody>
              ${plan.tasks.map((task, index) => `<tr data-task-row="${e(task.id)}"><td><strong class="et-planner-task-number">Task ${index + 1}</strong>${taskInput(task, index, 'Task name', 'title', 'maxlength="160" required')}${taskInput(task, index, 'Phase', 'phase', 'maxlength="80" required')}<small class="et-planner-id">ID: ${e(task.id)}</small></td>
                <td>${taskInput(task, index, 'Start date', 'start', 'type="date" min="1900-01-01" max="2199-12-31" required')}${taskInput(task, index, 'End date', 'end', 'type="date" min="1900-01-01" max="2199-12-31" required')}</td>
                <td>${taskInput(task, index, 'Estimated hours', 'hours', 'type="number" min="0" max="100000" step="0.01" inputmode="decimal" required')}${taskInput(task, index, 'Hourly rate', 'rate', 'type="number" min="0" max="1000000" step="0.01" inputmode="decimal" required')}</td>
                <td>${taskInput(task, index, 'Progress (%)', 'progressPercent', 'type="number" min="0" max="100" step="1" required')}<span class="et-planner-cost">${result ? e(money(result.taskCosts[task.id], plan.currency)) : 'Check inputs'}</span></td>
                <td><label><span class="et-planner-cell-label">Previous tasks</span><select aria-label="Task ${index + 1} previous tasks" data-task="${e(task.id)}" data-task-field="predecessors" data-focus="${e(task.id)}-predecessors" multiple size="3">${plan.tasks.filter(other => other.id !== task.id).map(other => `<option value="${e(other.id)}" ${task.predecessors.includes(other.id) ? 'selected' : ''}>${plan.tasks.findIndex(candidate => candidate.id === other.id) + 1}. ${e(other.title)}</option>`).join('')}</select></label><small>Choose any tasks that must finish first. Use Ctrl/⌘ to select more than one, or deselect.</small></td>
                <td><div class="et-planner-row-actions"><button type="button" data-action="up" data-id="${e(task.id)}" aria-label="Move task ${index + 1} up" ${index === 0 ? 'disabled' : ''}>↑</button><button type="button" data-action="down" data-id="${e(task.id)}" aria-label="Move task ${index + 1} down" ${index === plan.tasks.length - 1 ? 'disabled' : ''}>↓</button><button type="button" data-action="remove" data-id="${e(task.id)}" aria-label="Remove task ${index + 1}">Remove</button></div></td></tr>`).join('')}
            </tbody></table>
          </div>
          <p class="et-planner-help">${plan.tasks.length} of ${PLANNER_LIMITS.tasks} tasks. Dependencies mean finish-to-start, with no delay added. Working days are Monday–Friday; holidays are not deducted.</p>
        </section>
        <section class="et-planner-panel" aria-labelledby="et-plan-timeline">
          <div class="et-planner-heading"><div><h2 id="et-plan-timeline">Project timeline</h2><p class="et-planner-help" data-duration>${result ? `${e(result.start)} to ${e(result.end)}, inclusive · ${result.calendarDays} calendar days · ${result.workingDays} working days` : 'Correct the highlighted fields to see the updated timeline.'}</p></div><button type="button" data-action="chart" aria-expanded="${showChart}" aria-controls="et-planner-chart">${showChart ? 'Hide timeline' : 'Show timeline'}</button></div>
          <div id="et-planner-chart" ${showChart ? '' : 'hidden'}>
            <div class="et-planner-views" aria-label="Timeline scale">${(['Day', 'Week', 'Month'] as const).map(scale => `<button type="button" data-action="view" data-view="${scale}" aria-pressed="${view === scale}" ${scale === 'Day' && dayTooLong || scale === 'Week' && weekTooLong ? 'disabled' : ''}>${scale}</button>`).join('')}</div>
            <p class="et-planner-help">${weekTooLong ? 'This long plan uses Month view to keep the timeline responsive.' : dayTooLong ? 'Day view is available for plans up to two years long.' : 'On desktop, drag or resize a bar to change its dates. Use the task fields for keyboard or mobile editing.'}</p>
            <p data-chart-status class="et-planner-help" role="status">${result ? 'Loading the timeline…' : 'The timeline is paused while there are invalid fields.'}</p>
            <div data-chart class="et-planner-chart"></div>
          </div>
        </section>
        <section class="et-planner-panel" aria-labelledby="et-plan-save"><h2 id="et-plan-save">Save or share your plan</h2><p class="et-planner-help">Download a file to keep or share. The PDF includes full task details and separate timeline pages, so long plans stay readable. Your plan is not sent to Essential Designs.</p>
          <div class="et-planner-exports"><button type="button" data-action="json" ${!result || busy ? 'disabled' : ''}>Download JSON</button><button type="button" data-action="csv" ${!result || busy ? 'disabled' : ''}>Download CSV</button><label class="et-planner-field">PDF paper size<select data-page-size><option ${pageSize === 'A4' ? 'selected' : ''}>A4</option><option ${pageSize === 'Letter' ? 'selected' : ''}>Letter</option></select></label><button type="button" data-action="pdf" ${!result || busy ? 'disabled' : ''}>${busy ? 'Creating PDF…' : 'Download PDF brief'}</button></div>
          <label class="et-planner-remember"><input type="checkbox" data-remember ${remembered ? 'checked' : ''}> Remember this draft on this device</label><p class="et-planner-help">Off by default. Browser storage is not an encrypted vault; other people using this browser may be able to open the draft. Turning this off removes the saved copy.</p>
        </section>
      </div>`;
    for (const issue of errors) {
      const parts = issue.path.split('.');
      const focusKey = parts[0] === 'tasks' && parts.length > 2 ? `${plan.tasks[Number(parts[1])]?.id}-${parts[2]}` : `project-${parts[0]}`;
      root.querySelectorAll<HTMLElement>('[data-focus]').forEach(element => { if (element.dataset.focus === focusKey) element.setAttribute('aria-invalid', 'true'); });
    }
    if (active) root.querySelectorAll<HTMLElement>('[data-focus]').forEach(element => { if (element.dataset.focus === active) element.focus({ preventScroll: true }); });
    else if (activeButton?.action) {
      const replacement = [...root.querySelectorAll<HTMLButtonElement>('button[data-action]')].find(button => button.dataset.action === activeButton.action && button.dataset.id === activeButton.id && button.dataset.view === activeButton.view);
      (replacement ?? root.querySelector<HTMLButtonElement>('[data-action="add"]'))?.focus({ preventScroll: true });
    }
    if (result && showChart) void import('./chart').then(({ mountTimeline }) => {
      if (!alive || version !== renderVersion) return;
      const target = root.querySelector<HTMLElement>('[data-chart]')!;
      clearChart = mountTimeline(target, copy(plan), { view,
        onReady: () => { const status = root.querySelector<HTMLElement>('[data-chart-status]'); if (status) status.textContent = 'Timeline ready. The task list above provides the same dates and values.'; },
        onError: () => { const status = root.querySelector<HTMLElement>('[data-chart-status]'); if (status) status.textContent = 'The timeline could not load. You can still edit every date in the task list and export your plan.'; },
        onChange: (id, start, end) => {
          const candidate = copy(plan); const task = candidate.tasks.find(task => task.id === id);
          if (!task) return;
          task.start = start; task.end = end;
          const valid = validatePlan(candidate);
          if (!valid.ok) { tell('That timeline change was outside the supported date range.', true); render(); return; }
          plan = valid.value; dirty = true; tell('Task dates updated. Other tasks have not moved.'); persist(); render();
        },
      });
    }).catch(() => { if (alive && version === renderVersion) { const status = root.querySelector<HTMLElement>('[data-chart-status]'); if (status) status.textContent = 'The timeline could not load. Use the task list to continue.'; } });
  }

  function issueList(issues: Issue[], title: string, className: string) {
    if (!issues.length) return '';
    return `<div class="et-planner-issues ${className}" role="${className === 'et-error' ? 'alert' : 'status'}"><h3>${title}</h3><ul>${issues.slice(0, 12).map(issue => `<li>${e(issue.path.startsWith('tasks.') ? `Task ${Number(issue.path.split('.')[1]) + 1}: ` : '')}${e(issue.message)}</li>`).join('')}</ul>${issues.length > 12 ? `<p>And ${issues.length - 12} more. Correct these fields first.</p>` : ''}</div>`;
  }

  async function onChange(event: Event) {
    const input = event.target;
    if (!(input instanceof HTMLInputElement || input instanceof HTMLSelectElement || input instanceof HTMLTextAreaElement)) return;
    if (input.hasAttribute('data-project-field') || input.hasAttribute('data-task-field')) pendingInput = false;
    if (input.hasAttribute('data-import') && input instanceof HTMLInputElement) {
      const file = input.files?.[0]; if (!file) return;
      try {
        if (file.size > PLANNER_LIMITS.fileBytes) throw new InputError('This JSON file is too large. Use a plan smaller than 256 KB.');
        const checked = validatePlan(parseJson(await file.text(), PLANNER_LIMITS.fileBytes));
        if (!alive) return;
        if (!checked.ok) throw new InputError(`This plan could not be imported. ${checked.errors[0]?.message ?? 'Check its version and fields.'}`);
        if (!confirmReplace()) return;
        plan = checked.value; dirty = true; tell('Plan imported. Check the task dates and any schedule warnings.'); persist(); render();
      } catch (error) { tell(friendlyError(error), true); }
      finally { input.value = ''; }
      return;
    }
    if (input.hasAttribute('data-template')) {
      const selected = input.value as keyof typeof plannerExamples;
      if (selected && confirmReplace('Replace this plan with the sample project? Download JSON first if you want to keep your changes.')) {
        plan = copy(plannerExamples[selected]); dirty = true; tell('Sample project loaded. Its hours, rates and dates are illustrative.'); persist(); render();
      } else input.value = '';
      return;
    }
    if (input.hasAttribute('data-remember') && input instanceof HTMLInputElement) {
      remembered = input.checked;
      try {
        if (remembered) { const checked = validatePlan(plan); if (!checked.ok) throw new InputError('Correct the highlighted fields before saving a draft.'); saveDraft(storageKey, checked.value); tell('This draft will be saved in this browser as you edit it.'); }
        else { clearSavedDraft(storageKey); tell('The saved browser copy was removed. Your current plan is still open.'); }
      } catch (error) { remembered = false; tell(friendlyError(error), true); }
      render(); return;
    }
    if (input.hasAttribute('data-page-size')) { pageSize = input.value === 'Letter' ? 'Letter' : 'A4'; return; }
    const projectField = input.dataset.projectField;
    if (projectField && ['title', 'description', 'currency', 'referenceDate', 'contingencyPercent'].includes(projectField)) {
      plan = { ...plan, [projectField]: input.value }; dirty = true; tell('Project details updated.'); persist(); render(); return;
    }
    const taskId = input.dataset.task;
    const taskField = input.dataset.taskField as TaskField | undefined;
    if (taskId && taskField) {
      const task = plan.tasks.find(task => task.id === taskId); if (!task) return;
      if (taskField === 'predecessors' && input instanceof HTMLSelectElement) task.predecessors = Array.from(input.selectedOptions).map(option => option.value);
      else if (taskField !== 'predecessors') task[taskField] = input.value;
      dirty = true; tell('Task updated.'); persist(); render();
    }
  }

  async function onClick(event: MouseEvent) {
    const button = event.target instanceof Element ? event.target.closest<HTMLButtonElement>('button[data-action]') : null;
    if (!button || !root.contains(button) || button.disabled) return;
    const action = button.dataset.action;
    if (action === 'import') { root.querySelector<HTMLInputElement>('[data-import]')?.click(); return; }
    if (action === 'chart') { showChart = !showChart; render(); return; }
    if (action === 'view') { view = button.dataset.view as TimelineView; render(); return; }
    if (action === 'restore') {
      try {
        const saved = loadDraft(storageKey, validatePlan);
        if (!saved) { tell('There is no saved plan in this browser. You can import a JSON file instead.'); return; }
        if (!confirmReplace()) return;
        plan = saved; dirty = true; tell(remembered ? 'Your saved plan is open. Browser saving remains on.' : 'Your saved plan is open. Browser saving remains off unless you choose to enable it.'); render();
      } catch (error) { tell(friendlyError(error), true); }
      return;
    }
    if (action === 'clear') {
      if (!confirmReplace('Clear this plan and its saved browser copy? Download JSON first if you want to keep it.')) return;
      try { clearSavedDraft(storageKey); } catch (error) { tell(friendlyError(error), true); return; }
      const today = dateFromFrappe(new Date());
      plan = { schemaVersion: 1, kind: 'project-plan', title: 'Untitled project', description: '', currency: plan.currency, referenceDate: today, contingencyPercent: '0', tasks: [{ id: newTaskId(), phase: 'Planning', title: 'First task', start: today, end: today, hours: '0', rate: '0', progressPercent: '0', predecessors: [] }] };
      remembered = false; dirty = false; tell('The plan and saved browser copy have been cleared.'); render(); return;
    }
    if (action === 'add') {
      const last = plan.tasks.at(-1); const start = last && isDate(last.end) && last.end < '2199-12-24' ? nextWorkingDay(last.end) : plan.referenceDate;
      const id = newTaskId();
      plan.tasks.push({ id, phase: 'New phase', title: 'New task', start: isDate(start) ? start : '2026-09-07', end: isDate(start) ? start : '2026-09-07', hours: '0', rate: '0', progressPercent: '0', predecessors: [] });
      dirty = true; tell('Task added. Enter its dates, hours and rate.'); persist(); render();
      root.querySelector<HTMLInputElement>(`[data-focus="${id}-title"]`)?.focus(); return;
    }
    if (['up', 'down', 'remove'].includes(action ?? '')) {
      const index = plan.tasks.findIndex(task => task.id === button.dataset.id); if (index < 0) return;
      if (action === 'remove') {
        if (!confirmReplace('Remove this task and its links from other tasks? Their dates will stay the same.')) return;
        const id = plan.tasks[index].id; plan.tasks.splice(index, 1);
        plan.tasks.forEach(task => { task.predecessors = task.predecessors.filter(predecessor => predecessor !== id); });
      } else {
        const target = index + (action === 'up' ? -1 : 1);
        if (target < 0 || target >= plan.tasks.length) return;
        [plan.tasks[index], plan.tasks[target]] = [plan.tasks[target], plan.tasks[index]];
      }
      dirty = true; tell(action === 'remove' ? 'Task removed. Other task dates have not changed.' : 'Task order changed. Dates and dependencies are unchanged.'); persist(); render(); return;
    }
    if (['json', 'csv', 'pdf'].includes(action ?? '')) {
      const checked = validatePlan(plan);
      if (!checked.ok) { tell('Correct the highlighted fields before downloading.', true); return; }
      try {
        if (action === 'json') downloadJson(checked.value, 'project-plan-v1.json');
        else if (action === 'csv') downloadBlob(exportPlanCsv(checked.value), 'project-tasks.csv', 'text/csv;charset=utf-8');
        else {
          const exportRevision = inputRevision;
          const exportPageSize = pageSize;
          const exportSnapshot = JSON.stringify(checked.value);
          busy = true; tell('Creating the PDF brief and timeline pages…'); render();
          const [{ generatePlanPdf }, { fontBytes }] = await Promise.all([import('./pdf'), import('../shared/pdf')]);
          const bytes = await generatePlanPdf(checked.value, { fontBytes: await fontBytes() }, exportPageSize);
          if (!alive) return;
          if (exportRevision !== inputRevision || exportPageSize !== pageSize || exportSnapshot !== JSON.stringify(plan)) {
            tell('Your plan changed while the PDF was being prepared. Your edits are kept. Download the PDF again for the latest version.');
            return;
          }
          downloadBlob(bytes, 'project-plan.pdf', 'application/pdf');
        }
        tell(`${action === 'json' ? 'JSON' : action === 'csv' ? 'CSV' : 'PDF'} file created. Your browser handles the download.`);
        if (action === 'json') { dirty = false; pendingInput = false; }
      } catch (error) { tell(friendlyError(error), true); }
      finally { busy = false; if (alive) render(); }
    }
  }

  const onInput = (event: Event) => {
    const input = event.target;
    if (!(input instanceof HTMLInputElement || input instanceof HTMLSelectElement || input instanceof HTMLTextAreaElement)) return;
    const projectField = input.dataset.projectField;
    if (projectField && ['title', 'description', 'currency', 'referenceDate', 'contingencyPercent'].includes(projectField)) plan = { ...plan, [projectField]: input.value };
    else if (input.dataset.task && input.dataset.taskField) {
      const task = plan.tasks.find(task => task.id === input.dataset.task); if (!task) return;
      const field = input.dataset.taskField as TaskField;
      if (field === 'predecessors' && input instanceof HTMLSelectElement) task.predecessors = Array.from(input.selectedOptions).map(option => option.value);
      else if (field !== 'predecessors') task[field] = input.value;
    } else return;
    inputRevision++; pendingInput = true; dirty = true;
  };
  const beforeUnload = (event: BeforeUnloadEvent) => { if (pendingInput || dirty && (!remembered || !validatePlan(plan).ok)) { event.preventDefault(); event.returnValue = ''; } };
  root.addEventListener('input', onInput);
  root.addEventListener('change', onChange);
  root.addEventListener('click', onClick);
  window.addEventListener('beforeunload', beforeUnload);
  render();
  return () => { alive = false; renderVersion++; clearChart?.(); root.removeEventListener('input', onInput); root.removeEventListener('change', onChange); root.removeEventListener('click', onClick); window.removeEventListener('beforeunload', beforeUnload); root.replaceChildren(); };
}
