// screens-jobs.jsx

// Pick the most-meaningful timestamp + label for a job's current state.
// Returns { label, iso } - caller renders "<label> <relative-time>" with
// the full ISO as a tooltip.
function _jobTimeFor(job) {
  if (job.status === 'done'    && job.completed_at) return { label: 'done',    iso: job.completed_at };
  if (job.status === 'failed'  && job.failed_at)    return { label: 'failed',  iso: job.failed_at };
  if (job.status === 'running' && job.started_at)   return { label: 'started', iso: job.started_at };
  if (job.created_at)                                return { label: 'queued',  iso: job.created_at };
  return null;
}

function _fmtAbsolute(iso) {
  if (!iso) return '';
  try {
    return new Date(iso).toLocaleString(undefined, {
      month: 'short', day: 'numeric',
      hour: 'numeric', minute: '2-digit', second: '2-digit',
    });
  } catch { return iso; }
}

// Compact clock for the inline jobs cell - "2:23 PM" if today,
// "May 22 · 2:23 PM" if a different day. Full datetime stays in the tooltip.
function _fmtCompact(iso) {
  if (!iso) return '';
  try {
    const d = new Date(iso);
    const now = new Date();
    const sameDay = d.getFullYear() === now.getFullYear()
                 && d.getMonth()    === now.getMonth()
                 && d.getDate()     === now.getDate();
    const time = d.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
    if (sameDay) return time;
    const date = d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
    return date + ' · ' + time;
  } catch { return iso; }
}

function Jobs({ navigate }) {
  const [tab, setTab] = React.useState('all');
  const [jobs, setJobs] = React.useState(window.ZAMPP_DATA.JOBS);
  const [lastFetch, setLastFetch] = React.useState(Date.now());
  const [confirm, confirmModal] = window.useConfirm();

  const normalizeJob = (j) => {
    const statusMap = { waiting: 'queued', active: 'running', completed: 'done', failed: 'failed' };
    const kindMap = { proxy: 'Proxy', thumbnail: 'Thumbnail', conform: 'Conform', transcode: 'Transcode', import: 'YouTube', 'playout-stage': 'Stage' };
    const meta = j.metadata || {};
    return {
      ...j,
      status: statusMap[j.status] || j.status,
      kind: kindMap[j.type] || j.type || 'Job',
      asset: j.asset_name || meta.filename || '·',
      eta: '·',
      node: meta.node || '·',
      priority: meta.priority || 'normal',
      error: j.error || null,
      progress: j.progress || 0,
    };
  };

  const refresh = React.useCallback(() => {
    window.ZAMPP_API.fetch('/jobs')
      .then(raw => {
        const norm = (raw || []).map(normalizeJob);
        window.ZAMPP_DATA.JOBS = norm;
        setJobs(norm);
        setLastFetch(Date.now());
      })
      .catch(() => {});
  }, []);

  React.useEffect(() => {
    const i = setInterval(refresh, 5000);
    return () => clearInterval(i);
  }, [refresh]);

  const handleRetry = React.useCallback((job) => {
    window.ZAMPP_API.fetch('/jobs/' + job.id + '/retry', { method: 'POST' })
      .then(() => refresh())
      .catch(e => alert('Retry failed: ' + e.message));
  }, [refresh]);

  // One handler covers cancel (running) AND delete (queued / done / failed).
  // BullMQ's job.remove() - what the API calls - works on any state, so a
  // stalled-active job (worker died mid-process, holding a concurrency slot)
  // gets yanked and the next queued job runs. mode just changes the prompt
  // copy so the operator knows what they're doing.
  const handleDelete = React.useCallback(async (job, mode) => {
    const msg = mode === 'cancel'
      ? 'Cancel this running ' + job.kind + ' job?\n\nThe worker may run a few seconds longer in the background, but its result will be discarded and the queue slot frees up immediately.'
      : 'Remove this ' + job.status + ' ' + job.kind + ' job from the queue?';
    if (!(await confirm({
      title: mode === 'cancel' ? 'Cancel job?' : 'Remove job?',
      message: msg,
      confirmLabel: mode === 'cancel' ? 'Cancel job' : 'Remove',
    }))) return;
    window.ZAMPP_API.fetch('/jobs/' + job.id, { method: 'DELETE' })
      .then(() => setJobs(prev => prev.filter(j => j.id !== job.id)))
      .catch(e => alert((mode === 'cancel' ? 'Cancel' : 'Delete') + ' failed: ' + e.message));
  }, [confirm]);

  // Retry every failed job at once. Useful after a transient infra issue
  // (S3 outage, hung worker) - one click per job is painful with 20+ failures.
  const handleRetryAll = React.useCallback(async () => {
    const failedJobs = jobs.filter(j => j.status === 'failed');
    if (failedJobs.length === 0) return;
    if (!(await confirm({
      title: 'Re-queue failed jobs?',
      message: `Re-queue all ${failedJobs.length} failed jobs?`,
      confirmLabel: 'Re-queue',
      danger: false,
    }))) return;
    Promise.allSettled(
      failedJobs.map(j => window.ZAMPP_API.fetch('/jobs/' + j.id + '/retry', { method: 'POST' }))
    ).then(refresh);
  }, [jobs, refresh, confirm]);

  // Drop every failed job from the queue. The opposite of Retry all — used
  // when a batch of jobs is unrecoverable (e.g. assets that were deleted
  // mid-encode) and the operator just wants the queue cleared.
  const handleCancelAll = React.useCallback(async () => {
    const failedJobs = jobs.filter(j => j.status === 'failed');
    if (failedJobs.length === 0) return;
    if (!(await confirm({
      title: 'Remove all failed jobs?',
      message: `Remove all ${failedJobs.length} failed jobs from the queue?\nThis cannot be undone.`,
      confirmLabel: 'Remove all',
    }))) return;
    Promise.allSettled(
      failedJobs.map(j => window.ZAMPP_API.fetch('/jobs/' + j.id, { method: 'DELETE' }))
    ).then(() => {
      // Optimistic local drop so the UI updates the instant the modal closes,
      // not 5s later on the next poll tick.
      setJobs(prev => prev.filter(j => j.status !== 'failed'));
      refresh();
    });
  }, [jobs, refresh, confirm]);

  const counts = {
    all:     jobs.length,
    running: jobs.filter(j => j.status === 'running').length,
    queued:  jobs.filter(j => j.status === 'queued').length,
    done:    jobs.filter(j => j.status === 'done').length,
    failed:  jobs.filter(j => j.status === 'failed').length,
  };
  const filtered = tab === 'all' ? jobs : jobs.filter(j => j.status === tab);

  return (
    <div className="page">
      {confirmModal}
      <div className="page-header">
        <h1>Jobs</h1>
        <span className="subtitle">Proxy generation, transcoding, and processing queue</span>
        <div className="spacer" />
        {counts.failed > 0 && (
          <>
            <button className="btn ghost sm" onClick={handleRetryAll} title={`Retry all ${counts.failed} failed jobs`}>
              <Icon name="refresh" />Retry all failed
            </button>
            <button className="btn ghost sm jobs-cancel-all" onClick={handleCancelAll} title={`Remove all ${counts.failed} failed jobs from the queue`}>
              <Icon name="x" />Cancel all failed
            </button>
          </>
        )}
        <button className="btn ghost sm" onClick={refresh}>
          <Icon name="refresh" />Refresh
        </button>
      </div>

      <div className="page-body">
        <div className="jobs-stats">
          <div className={'stat-card' + (counts.running > 0 ? ' stat-card--active' : '')}>
            <div className="label">Running</div>
            <div className="value">{counts.running}</div>
            <div className="delta">{counts.queued} queued</div>
          </div>
          <div className="stat-card">
            <div className="label">Completed</div>
            <div className="value stat-value--muted">{counts.done}</div>
            <div className="delta">Total done</div>
          </div>
          <div className={'stat-card' + (counts.failed > 0 ? ' stat-card--failed' : '')}>
            <div className="label">Failed</div>
            <div className={'value' + (counts.failed > 0 ? ' stat-value--danger' : ' stat-value--muted')}>{counts.failed}</div>
            <div className={'delta' + (counts.failed > 0 ? ' delta-warn' : '')}>
              {counts.failed > 0 ? 'Needs attention' : 'All clear'}
            </div>
          </div>
          <div className="stat-card stat-card--quiet">
            <div className="label">Total jobs</div>
            <div className="value stat-value--muted">{counts.all}</div>
            <div className="delta muted delta-tiny">Updated {Math.round((Date.now()-lastFetch)/1000)}s ago</div>
          </div>
        </div>

        <div className="tab-group jobs-tabs">
          {[
            { id: 'all',     label: 'All · ' + counts.all },
            { id: 'running', label: 'Running · ' + counts.running },
            { id: 'queued',  label: 'Queued · ' + counts.queued },
            { id: 'done',    label: 'Done · ' + counts.done },
            { id: 'failed',  label: 'Failed · ' + counts.failed },
          ].map(t => (
            <button key={t.id} className={tab === t.id ? 'active' : ''} onClick={() => setTab(t.id)}>{t.label}</button>
          ))}
        </div>

        <div className="panel jobs-panel">
          <div className="job-row head">
            <div></div><div>Job</div><div>Asset</div><div>Node</div><div>Progress</div><div>Time</div><div>Priority</div><div></div>
          </div>
          {filtered.length === 0
            ? <div className="jobs-empty">No jobs in this category.</div>
            : filtered.map(j => <JobRow key={j.id} job={j} onRetry={handleRetry} onDelete={handleDelete} />)}
        </div>
      </div>
    </div>
  );
}

function JobRow({ job, onRetry, onDelete }) {
  const iconMap = { Proxy: 'proxy', Transcode: 'film', Thumbnail: 'image', Conform: 'layers', Stage: 'monitor' };
  return (
    <div className="job-row">
      <div><StatusDot status={job.status} /></div>
      <div className="job-row-kind">
        <Icon name={iconMap[job.kind] || 'jobs'} size={13} className="job-row-kind-icon" />
        <span className="job-row-kind-name">{job.kind}</span>
      </div>
      <div className="job-row-asset">{job.asset}</div>
      <div className="mono job-row-node">{job.node}</div>
      <div>
        {job.status === 'running' && (
          <div className="job-progress-wrap">
            <div className="job-progress-bar"><div className="job-progress-fill" style={{ width: job.progress + '%' }} /></div>
            <span className="mono job-row-progress-pct">{Math.round(job.progress)}%</span>
          </div>
        )}
        {job.status === 'done' && <span className="badge success job-row-status-done"><Icon name="check" size={12} /> Complete</span>}
        {job.status === 'queued' && <span className="job-row-status-queued">Waiting…</span>}
        {job.status === 'failed' && (
          <span title={job.error || 'Failed'} className="job-row-status-failed">
            <Icon name="alert" size={12} />
            <span className="job-row-status-failed-msg">
              {(job.error || 'Failed').slice(0, 120)}
            </span>
          </span>
        )}
      </div>
      <div className="mono job-row-time"
           title={(() => { const t = _jobTimeFor(job); return t ? t.label + ' at ' + _fmtAbsolute(t.iso) : ''; })()}>
        {(() => {
          const t = _jobTimeFor(job);
          if (!t) return '·';
          // Terminal states (done/failed) anchor on the absolute clock so the
          // operator can correlate with logs; queued/running show relative
          // since it's a moving target.
          if (job.status === 'done' || job.status === 'failed') {
            return t.label + ' ' + _fmtCompact(t.iso) + ' · ' + window.ZAMPP_API.fmtRelative(t.iso);
          }
          return t.label + ' ' + window.ZAMPP_API.fmtRelative(t.iso);
        })()}
      </div>
      <div><span className={'badge ' + (job.priority === 'high' ? 'warning' : 'outline')}>{job.priority}</span></div>
      <div className="job-row-actions">
        {job.status === 'failed' && (
          <button className="btn ghost sm" onClick={() => onRetry(job)}><Icon name="refresh" />Retry</button>
        )}
        {job.status === 'running' && (
          /* Cancel a stalled-active job: frees the BullMQ concurrency slot
             so anything queued behind it can run. The worker may finish in
             the background but its result is discarded. */
          <button className="btn ghost sm job-row-cancel" onClick={() => onDelete(job, 'cancel')}
            title="Cancel this running job and free its queue slot">
            <Icon name="x" />Cancel
          </button>
        )}
        {(job.status === 'queued' || job.status === 'done' || job.status === 'failed') && (
          <button className="icon-btn" aria-label="Remove job from the queue" title="Remove job from the queue"
            onClick={() => onDelete(job, 'delete')}><Icon name="x" /></button>
        )}
      </div>
    </div>
  );
}

window.Jobs = Jobs;
