// Task 9 — Pattern-based bulk image attribute editor with dry-run, apply,
// and rollback. Surfaces the /image-attributes REST endpoints. No marketing
// copy; reuses the surrounding pr:* utility classes.

import { useEffect, useState, useCallback, useMemo } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import apiFetch from '@wordpress/api-fetch';

const TOKENS = ['{filename}', '{attachment_title}', '{parent_title}', '{site_name}', '{mime_type}', '{year}'];
const FIELDS = ['alt', 'title', 'caption'];

const BulkImageAttributes = () => {
  const [templates, setTemplates] = useState({
    alt: '{filename} {site_name}',
    title: '{filename}',
    caption: '',
  });
  const [fields, setFields] = useState({ alt: true, title: false, caption: false });
  const [mode, setMode] = useState('only_missing');
  const [scope, setScope] = useState('missing_alt');
  const [attachmentIds, setAttachmentIds] = useState([]);
  const [preview, setPreview] = useState(null);
  const [running, setRunning] = useState(false);
  const [history, setHistory] = useState([]);
  const [historyMax, setHistoryMax] = useState(50);
  const [message, setMessage] = useState('');
  // R1 — server returns a single-use dry_run_id; apply is disabled until
  // we have one whose body-shape matches the user's current inputs.
  const [dryRunId, setDryRunId] = useState('');
  const [previewKey, setPreviewKey] = useState('');

  const fetchHistory = useCallback(async () => {
    try {
      const res = await apiFetch({ path: '/prorank-seo/v1/image-attributes/history', method: 'GET' });
      const data = res?.data ?? res ?? {};
      if (Array.isArray(data.runs)) setHistory(data.runs);
      if (typeof data.history_max === 'number') setHistoryMax(data.history_max);
    } catch (e) { /* swallow — UI shows empty list */ }
  }, []);

  useEffect(() => { fetchHistory(); }, [fetchHistory]);

  const loadCandidates = useCallback(async () => {
    setMessage('');
    try {
      const res = await apiFetch({
        path: `/prorank-seo/v1/image-attributes/candidates?scope=${encodeURIComponent(scope)}&limit=100`,
        method: 'GET',
      });
      const data = res?.data ?? res ?? {};
      const ids = Array.isArray(data.ids) ? data.ids : [];
      setAttachmentIds(ids);
      setMessage(sprintf(
        // translators: 1: number of attachment ids matched
        __('Loaded %d attachments for the selected scope.', 'prorank-seo'),
        ids.length
      ));
    } catch (e) {
      setMessage(__('Could not load candidates.', 'prorank-seo'));
    }
  }, [scope]);

  const enabledFields = useMemo(() => FIELDS.filter((f) => fields[f]), [fields]);

  const buildRequestBody = useCallback(() => ({
    templates: {
      alt:     fields.alt     ? templates.alt     : '',
      title:   fields.title   ? templates.title   : '',
      caption: fields.caption ? templates.caption : '',
    },
    fields: enabledFields,
    mode,
    attachment_ids: attachmentIds,
  }), [templates, fields, enabledFields, mode, attachmentIds]);

  // R1 — stable hash of the request body shape so the Apply button only
  // re-enables when the user re-runs Dry-run after editing inputs.
  const currentInputKey = useMemo(
    () => JSON.stringify(buildRequestBody()),
    [buildRequestBody]
  );
  // Invalidate any held token whenever the inputs change.
  useEffect(() => {
    if (dryRunId && previewKey && previewKey !== currentInputKey) {
      setDryRunId('');
    }
  }, [currentInputKey, previewKey, dryRunId]);

  const doDryRun = useCallback(async () => {
    if (attachmentIds.length === 0) {
      setMessage(__('Load some attachments first.', 'prorank-seo'));
      return;
    }
    setRunning(true);
    setMessage('');
    try {
      const body = buildRequestBody();
      const res = await apiFetch({
        path: '/prorank-seo/v1/image-attributes/dry-run',
        method: 'POST',
        data: body,
      });
      const data = res?.data ?? res ?? null;
      setPreview(data);
      // R1 — record the freshly-minted token + the input snapshot it was
      // built against. Apply is only enabled while both are in sync.
      setDryRunId((data && data.dry_run_id) ? String(data.dry_run_id) : '');
      setPreviewKey(JSON.stringify(body));
    } catch (e) {
      setMessage(__('Dry-run failed.', 'prorank-seo'));
    } finally { setRunning(false); }
  }, [attachmentIds, buildRequestBody]);

  const doApply = useCallback(async () => {
    if (attachmentIds.length === 0) return;
    if (!dryRunId) {
      setMessage(__('Run Dry-run first — Apply requires a fresh preview.', 'prorank-seo'));
      return;
    }
    setRunning(true);
    setMessage('');
    try {
      const res = await apiFetch({
        path: '/prorank-seo/v1/image-attributes/apply',
        method: 'POST',
        data: { ...buildRequestBody(), dry_run_id: dryRunId },
      });
      const data = res?.data ?? res ?? {};
      setPreview(data);
      setMessage(sprintf(
        // translators: 1: applied count 2: skipped count
        __('Applied to %1$d attachments, %2$d skipped.', 'prorank-seo'),
        data.applied || 0, data.skipped || 0
      ));
      // Token is single-use — clear local copy so a second click can't
      // try to replay it against a server transient that is already gone.
      setDryRunId('');
      setPreviewKey('');
      fetchHistory();
    } catch (e) {
      // The server raises a 409 with a code like dry_run_expired_or_unknown
      // / dry_run_drift / dry_run_user_mismatch. Surface a useful nudge.
      const reason = e?.code ?? '';
      setDryRunId('');
      setPreviewKey('');
      if (reason && reason.startsWith('dry_run_')) {
        setMessage(__('Apply rejected — re-run Dry-run to refresh the preview.', 'prorank-seo'));
      } else {
        setMessage(__('Apply failed.', 'prorank-seo'));
      }
    } finally { setRunning(false); }
  }, [attachmentIds, buildRequestBody, fetchHistory, dryRunId]);

  const doRollback = useCallback(async (runId) => {
    if (!runId) return;
    setMessage('');
    try {
      const res = await apiFetch({
        path: `/prorank-seo/v1/image-attributes/rollback/${encodeURIComponent(runId)}`,
        method: 'POST',
      });
      const data = res?.data ?? res ?? {};
      const restored = data.restored || 0;
      const conflict = data.conflict || 0;
      setMessage(sprintf(
        // translators: 1: restored count 2: conflict count
        __('Rolled back %1$d rows, %2$d skipped due to conflicts.', 'prorank-seo'),
        restored, conflict
      ));
      fetchHistory();
    } catch (e) {
      setMessage(__('Rollback failed.', 'prorank-seo'));
    }
  }, [fetchHistory]);

  return (
    <div className="pr:flex pr:flex-col pr:gap-4">
      <h3 className="pr:text-base pr:font-semibold">{__('Bulk Image Attributes', 'prorank-seo')}</h3>

      {/* Template inputs */}
      <div className="pr:flex pr:flex-col pr:gap-3">
        {FIELDS.map((field) => (
          <div key={field} className="pr:flex pr:items-center pr:gap-2">
            <label className="pr:flex pr:items-center pr:gap-2 pr:w-24">
              <input
                type="checkbox"
                checked={!!fields[field]}
                onChange={(e) => setFields((p) => ({ ...p, [field]: e.target.checked }))}
              />
              <span className="pr:capitalize">{field}</span>
            </label>
            <input
              type="text"
              className="pr:flex-1 pr:p-2 pr:border pr:border-gray-300 pr:rounded-xs pr:font-mono pr:text-xs"
              placeholder={field === 'alt' ? '{filename} {site_name}' : '{filename}'}
              value={templates[field]}
              onChange={(e) => setTemplates((p) => ({ ...p, [field]: e.target.value }))}
              disabled={!fields[field]}
            />
          </div>
        ))}
        <div className="pr:flex pr:flex-wrap pr:gap-1 pr:text-xs pr:text-gray-600">
          {TOKENS.map((tok) => (
            <code key={tok} className="pr:px-1.5 pr:py-0.5 pr:bg-gray-100 pr:rounded-xs">{tok}</code>
          ))}
        </div>
      </div>

      {/* Mode + scope + load */}
      <div className="pr:flex pr:flex-wrap pr:items-center pr:gap-4">
        <div className="pr:flex pr:items-center pr:gap-2">
          <label className="pr:flex pr:items-center pr:gap-1">
            <input
              type="radio"
              name="prorank-bulk-mode"
              value="only_missing"
              checked={mode === 'only_missing'}
              onChange={() => setMode('only_missing')}
            />
            <span>{__('Only missing', 'prorank-seo')}</span>
          </label>
          <label className="pr:flex pr:items-center pr:gap-1">
            <input
              type="radio"
              name="prorank-bulk-mode"
              value="overwrite"
              checked={mode === 'overwrite'}
              onChange={() => setMode('overwrite')}
            />
            <span>{__('Overwrite existing', 'prorank-seo')}</span>
          </label>
        </div>
        <div className="pr:flex pr:items-center pr:gap-2">
          <select
            className="pr:p-1.5 pr:border pr:border-gray-300 pr:rounded-xs pr:text-sm"
            value={scope}
            onChange={(e) => setScope(e.target.value)}
          >
            <option value="missing_alt">{__('Scope: missing alt', 'prorank-seo')}</option>
            <option value="has_alt">{__('Scope: has alt', 'prorank-seo')}</option>
            <option value="generated">{__('Scope: generated by ProRank', 'prorank-seo')}</option>
          </select>
          <button
            type="button"
            className="pr:px-3 pr:py-1.5 pr:border pr:border-gray-300 pr:rounded-xs pr:bg-white pr:text-sm"
            onClick={loadCandidates}
            disabled={running}
          >
            {__('Load attachments', 'prorank-seo')}
          </button>
          <span className="pr:text-xs pr:text-gray-600">
            {sprintf(
              // translators: 1: number of attachment ids
              __('%d selected', 'prorank-seo'),
              attachmentIds.length
            )}
          </span>
        </div>
      </div>

      {/* Action buttons */}
      <div className="pr:flex pr:items-center pr:gap-2">
        <button
          type="button"
          className="pr:px-3 pr:py-1.5 pr:bg-primary-50 pr:border pr:border-primary-200 pr:rounded-xs pr:text-sm"
          onClick={doDryRun}
          disabled={running || attachmentIds.length === 0 || enabledFields.length === 0}
        >
          {running ? __('Working…', 'prorank-seo') : __('Dry-run', 'prorank-seo')}
        </button>
        <button
          type="button"
          className="pr:px-3 pr:py-1.5 pr:bg-primary-600 pr:text-white pr:rounded-xs pr:text-sm pr:disabled:opacity-50"
          onClick={doApply}
          disabled={
            running
            || attachmentIds.length === 0
            || enabledFields.length === 0
            || !dryRunId
            || previewKey !== currentInputKey
          }
          title={!dryRunId ? __('Run Dry-run first.', 'prorank-seo') : ''}
        >
          {__('Apply', 'prorank-seo')}
        </button>
        {!dryRunId && (
          <span className="pr:text-xs pr:text-gray-500">
            {__('Apply requires a fresh dry-run.', 'prorank-seo')}
          </span>
        )}
        {message && <span className="pr:text-xs pr:text-gray-600">{message}</span>}
      </div>

      {/* Preview table */}
      {preview && Array.isArray(preview.items) && preview.items.length > 0 && (
        <div className="pr:border pr:border-gray-200 pr:rounded-xs pr:overflow-x-auto">
          <table className="pr:w-full pr:text-xs">
            <thead className="pr:bg-gray-50">
              <tr>
                <th className="pr:text-left pr:px-2 pr:py-1">ID</th>
                <th className="pr:text-left pr:px-2 pr:py-1">{__('Field', 'prorank-seo')}</th>
                <th className="pr:text-left pr:px-2 pr:py-1">{__('Before', 'prorank-seo')}</th>
                <th className="pr:text-left pr:px-2 pr:py-1">{__('After', 'prorank-seo')}</th>
                <th className="pr:text-left pr:px-2 pr:py-1">{__('Status', 'prorank-seo')}</th>
              </tr>
            </thead>
            <tbody>
              {preview.items.slice(0, 50).flatMap((row) => {
                if (row.skipped) {
                  return [(
                    <tr key={`${row.id}-skip`} className="pr:border-t pr:border-gray-100">
                      <td className="pr:px-2 pr:py-1">{row.id}</td>
                      <td className="pr:px-2 pr:py-1" colSpan={3}>{row.filename || '—'}</td>
                      <td className="pr:px-2 pr:py-1 pr:text-gray-500">{row.skipped_reason}</td>
                    </tr>
                  )];
                }
                return FIELDS.filter((f) => row.fields_to_change?.[f]).map((f) => (
                  <tr key={`${row.id}-${f}`} className="pr:border-t pr:border-gray-100">
                    <td className="pr:px-2 pr:py-1">{row.id}</td>
                    <td className="pr:px-2 pr:py-1">{f}</td>
                    <td className="pr:px-2 pr:py-1 pr:text-gray-500">{row.before?.[f] || '∅'}</td>
                    <td className="pr:px-2 pr:py-1">{row.after?.[f] || '∅'}</td>
                    <td className="pr:px-2 pr:py-1">{row.applied ? __('applied', 'prorank-seo') : __('preview', 'prorank-seo')}</td>
                  </tr>
                ));
              })}
            </tbody>
          </table>
          {preview.capped && (
            <div className="pr:px-2 pr:py-1 pr:text-xs pr:text-gray-600">
              {__('Capped to 100 attachments per run.', 'prorank-seo')}
            </div>
          )}
        </div>
      )}

      {/* History */}
      <div className="pr:flex pr:flex-col pr:gap-2">
        <h4 className="pr:text-sm pr:font-semibold">
          {sprintf(
            // translators: 1: count 2: max
            __('Recent runs (%1$d of %2$d)', 'prorank-seo'),
            history.length, historyMax
          )}
        </h4>
        {history.length === 0 ? (
          <p className="pr:text-xs pr:text-gray-500">{__('No runs yet.', 'prorank-seo')}</p>
        ) : (
          <ul className="pr:flex pr:flex-col pr:gap-1">
            {history.map((run) => {
              const status = run.status || 'applied';
              const isRolledBack = status === 'rolled_back';
              return (
                <li key={run.run_id} className="pr:flex pr:items-center pr:justify-between pr:p-2 pr:border pr:border-gray-100 pr:rounded-xs pr:text-xs">
                  <div className="pr:flex pr:flex-col">
                    <code className="pr:font-mono pr:text-[11px]">{run.run_id}</code>
                    <span className="pr:text-gray-600">
                      {run.mode} · {(run.fields || []).join(', ')} · {run.change_count} {__('rows', 'prorank-seo')}
                      {status !== 'applied' && (
                        <span className="pr:ml-2 pr:px-1.5 pr:py-0.5 pr:rounded-xs pr:bg-gray-100 pr:text-gray-700 pr:uppercase pr:tracking-wide pr:text-[10px]">
                          {status}
                        </span>
                      )}
                    </span>
                  </div>
                  <button
                    type="button"
                    className="pr:px-2 pr:py-1 pr:border pr:border-gray-300 pr:rounded-xs pr:bg-white pr:disabled:opacity-50"
                    onClick={() => doRollback(run.run_id)}
                    disabled={isRolledBack}
                    title={isRolledBack ? __('This run has already been fully rolled back.', 'prorank-seo') : ''}
                  >
                    {isRolledBack ? __('Rolled back', 'prorank-seo') : __('Rollback', 'prorank-seo')}
                  </button>
                </li>
              );
            })}
          </ul>
        )}
      </div>
    </div>
  );
};

export default BulkImageAttributes;
