/**
 * Per-file sync status shown in the file tree's Status column.
 *
 * The status itself is computed by cambium-core's engine (see platform.js's
 * `computeStatus`, invoking the Rust `compute_status` command): this module
 * only turns each `{path, status, detail}` engine entry into the chip the
 * tree renders (icon/color/label/title/text), plus the collapsed-folder
 * rollup and suppression rules that stay presentation-only.
 */

export const STATUS_KINDS = {
  modified: {
    icon: 'assets/circle_exclaim.svg',
    color: 'orange',
    label: 'Modified',
    title: 'File has changes',
    explain: () =>
      'This file has changes but is not checked out: its contents no longer match ' +
      'the version it was based on. Check the file out to lock it and keep ' +
      'working on it, or pull latest to replace it with the server version.',
  },
  // The local copy is clean (matches the version it was pulled at) but a
  // teammate has since submitted a newer revision — distinct from "modified":
  // the user has nothing to lose, they just need to pull.
  outdated: {
    icon: 'assets/circle_exclaim.svg',
    color: 'orange',
    label: 'Outdated',
    title: 'File is out of date',
    explain: (headRevision) => {
      const target = headRevision != null ? `revision ${headRevision}` : 'a newer revision';
      return (
        `Someone submitted ${target} of this file, so the copy in your workspace ` +
        'is no longer the newest version. Pull latest to download the new version ' +
        'before working with it.'
      );
    },
  },
  checkedOutByMe: {
    icon: 'assets/circle_check.svg',
    color: 'pale-green',
    label: 'Checked out',
    title: 'File is checked out to you',
    explain: () =>
      'You hold the lock on this file, so it is writable here and nobody else can ' +
      'submit changes to it. You can either revert it to restore it to how it was ' +
      'before you checked it out, or submit it in your next changelist or on its own.',
  },
  notAdded: {
    icon: 'assets/circle_add.svg',
    color: 'red',
    label: 'Not added',
    title: 'File has not been added to the server yet',
    explain: () =>
      'This file only exists in your workspace — the server does not know about it. ' +
      'Click the Add New button to mark it for add, then submit it to create it on ' +
      'the server.',
  },
  markedForAdd: {
    icon: 'assets/circle_add.svg',
    color: 'green',
    label: 'Marked for add',
    title: 'File has been marked for add',
    explain: () =>
      'This file is in your changelist as a new file. It will be uploaded to the ' +
      "server when you submit. Click the Don't Add button if you no longer want " +
      'to add it.',
  },
  // A local file whose name breaks a cross-filesystem rule — it can't be
  // submitted until it's renamed, so it never turns green for add.
  namingIssue: {
    icon: 'assets/circle_exclaim.svg',
    color: 'orange',
    label: 'Naming issue',
    title: 'File name breaks a naming rule',
    explain: (reason) =>
      'You will not be able to submit this file because its name breaks a naming ' +
      'rule: ' +
      (reason ? `${reason}. ` : '. ') +
      'Different operating systems (Windows, macOS, Linux) allow different names, ' +
      'and this one would fail on at least one of them. Rename the file, then add ' +
      'and submit it.',
  },
  lockedByOther: {
    icon: 'assets/locked_individual.svg',
    color: 'orange',
    label: 'Checked out by',
    title: 'File is checked out',
    explain: (holder) =>
      `This file is checked out by ${holder}, so it is locked: you cannot check it ` +
      'out or submit changes to it until they release it. If the lock is stale, a ' +
      'server admin can force-unlock it.',
  },
  lockedByMeElsewhere: {
    icon: 'assets/locked_individual.svg',
    color: 'orange',
    label: 'Checked out by you on',
    title: 'File is checked out on another device',
    explain: (deviceName) =>
      `You checked this file out on your device “${deviceName}”. Locks are ` +
      'per-device — each device is its own workspace — so this device cannot ' +
      'edit or submit the file. Release or submit it from ' +
      `“${deviceName}”, or ask a server admin to force-unlock it.`,
  },
  markedForDelete: {
    icon: 'assets/delete.svg',
    color: 'red',
    label: 'Marked for delete',
    title: 'File has been marked for delete',
    explain: () =>
      'This file is in your changelist as a delete: when you submit, it will be ' +
      'removed from the repository. Its local copy has already been removed from ' +
      'your drive — the struck-through row just shows where it was. Revert it to ' +
      'restore the file and cancel the delete.',
  },
};

/** Collapsed-folder chip when its files carry different (or partial) statuses. */
export const MIXED_FOLDER_STATUS = {
  kind: 'mixed',
  icon: 'assets/circle_exclaim.svg',
  color: 'grey',
  label: 'Open for details',
  title: 'Files inside have mixed statuses',
  text:
    'Some files inside this folder carry a status (checked out, modified, marked ' +
    'for add or delete…) but they do not all share the same one. Expand the folder ' +
    'to see the status on each file.',
};

/** Folder whose files are all individually marked for delete — distinct from a
 * folder that is itself marked: the folder stays on the server (and is not
 * struck through) unless the user marks the folder too. */
export const CONTENTS_MARKED_STATUS = {
  kind: 'contentsMarkedForDelete',
  icon: 'assets/delete.svg',
  color: 'red',
  label: 'Contents marked for delete',
  title: 'Contents marked for delete',
  text:
    'Every file inside this folder is marked for delete: the local copies are ' +
    'already gone and they will be removed from the repository when you submit. ' +
    'The folder itself is not marked — select the folder and click Delete to ' +
    'remove the folder too, or revert the files to restore them.',
};

/** Chip for a folder the user marked for delete (the whole subtree goes). */
export function folderMarkedStatus() {
  const kind = STATUS_KINDS.markedForDelete;
  return {
    kind: 'markedForDelete',
    icon: kind.icon,
    color: kind.color,
    label: kind.label,
    title: 'Folder has been marked for delete',
    text:
      'This folder is in your changelist as a delete: it and everything inside it ' +
      'will be removed from the repository when you submit. The local copies have ' +
      'already been removed from your drive. Revert it to restore the folder and ' +
      'cancel the delete.',
  };
}

/**
 * Roll a collapsed folder's descendant file statuses up onto the folder row so
 * nothing checked out or marked ever hides inside a closed folder: every file
 * shares one status → that status; some/conflicting statuses → MIXED_FOLDER_STATUS;
 * no file has one → no chip.
 *
 * @param node     folder tree node (children carry ids: rootId + relPath)
 * @param statuses Map relPath → chip
 * @param rootId   id prefix to strip
 */
export function rollupFolderStatus(node, statuses, rootId) {
  let uniform;
  let sawFile = false;
  let mixed = false;

  const visit = (n) => {
    if (mixed) return;
    if (n.type === 'file') {
      const rel = n.id.startsWith(rootId) ? n.id.slice(rootId.length) : null;
      const status = rel ? (statuses.get(rel) ?? null) : null;
      if (!sawFile) {
        sawFile = true;
        uniform = status;
      } else if ((uniform?.label ?? null) !== (status?.label ?? null)) {
        mixed = true;
      }
      return;
    }
    n.children?.forEach(visit);
  };
  node.children?.forEach(visit);

  if (!sawFile) return null;
  if (mixed) return MIXED_FOLDER_STATUS;
  // Every file individually marked ≠ the folder itself being marked: the
  // folder survives the submit unless the user marks it too.
  if (uniform?.kind === 'markedForDelete') return CONTENTS_MARKED_STATUS;
  return uniform ?? null;
}

function chip(kindKey) {
  const kind = STATUS_KINDS[kindKey];
  return {
    kind: kindKey,
    icon: kind.icon,
    color: kind.color,
    label: kind.label,
    title: kind.title,
    text: kind.explain(),
  };
}

/** Naming-issue chip carrying the specific rule the file's name broke. */
function namingChip(reason) {
  const kind = STATUS_KINDS.namingIssue;
  return {
    kind: 'namingIssue',
    icon: kind.icon,
    color: kind.color,
    label: kind.label,
    title: kind.title,
    text: kind.explain(reason),
  };
}

/** Outdated chip enriched with the depot head revision the file has fallen
 * behind; the info popup urges pulling the new version. */
function outdatedChip(headRevision) {
  const kind = STATUS_KINDS.outdated;
  return {
    kind: 'outdated',
    icon: kind.icon,
    color: kind.color,
    label: kind.label,
    title:
      headRevision != null
        ? `Out of date — server has revision ${headRevision}`
        : kind.title,
    text: kind.explain(headRevision),
  };
}

/** Locked-by-someone-else chip; `holder` is already the resolved display name
 * (`detail.holderDisplayName` from the engine — see gui.rs's render_status). */
function lockedByOtherChip(holder) {
  const kind = STATUS_KINDS.lockedByOther;
  return {
    kind: 'lockedByOther',
    icon: kind.icon,
    color: kind.color,
    label: `${kind.label} ${holder}`,
    title: `File is checked out by ${holder}`,
    text: kind.explain(holder),
  };
}

/** Locked by the same account on a different device. */
function lockedByMeElsewhereChip(deviceName) {
  const kind = STATUS_KINDS.lockedByMeElsewhere;
  return {
    kind: 'lockedByMeElsewhere',
    icon: kind.icon,
    color: kind.color,
    label: `${kind.label} ${deviceName}`,
    title: `File is checked out by you on ${deviceName}`,
    text: kind.explain(deviceName),
  };
}

/**
 * Turn one engine entry (`{path, status, detail}`, cambium-core's
 * `compute_status --json` vocabulary) into the chip FileTree renders.
 * Returns null for a status key the chip set doesn't recognize.
 */
export function chipFor(entry) {
  const detail = entry.detail ?? null;
  switch (entry.status) {
    case 'modified':
      return chip('modified');
    case 'outdated':
      return outdatedChip(detail?.headRevision);
    case 'checkedOutByMe':
      return chip('checkedOutByMe');
    case 'notAdded':
      return chip('notAdded');
    case 'markedForAdd':
      return chip('markedForAdd');
    case 'markedForDelete':
      return chip('markedForDelete');
    case 'namingIssue':
      return namingChip(detail?.reason);
    case 'lockedByOther':
      return lockedByOtherChip(detail?.holderDisplayName);
    case 'lockedByMeElsewhere':
      return lockedByMeElsewhereChip(detail?.deviceName);
    default:
      return null;
  }
}

/**
 * Build the path → chip map FileTree renders from the engine's raw entries.
 *
 * The engine (cambium-core) always reports `notAdded` for an un-added local
 * file — it has no notion of an in-flight pull or a not-yet-fetched depot
 * tree, both GUI-only conditions. Those two suppressions are applied here
 * instead, exactly as the pre-engine client-side status builder did:
 *   - `serverReady: false` (depot tree not fetched yet): suppress notAdded.
 *   - `pullingPaths` (paths an active pull is downloading): suppress notAdded
 *     for a path currently landing, so it doesn't flash "not added" a beat
 *     before the pull's own bookkeeping catches up.
 * markedForAdd and namingIssue are never suppressed — a bad name or an
 * intentional add should never be hidden by a concurrent pull.
 *
 * @param entries Array of `{path, status, detail}` engine entries.
 * @param options.pullingPaths Set of relPaths an in-flight pull is landing (or null).
 * @param options.serverReady  Whether the depot tree has been fetched at least once.
 */
export function buildChipMap(entries, { pullingPaths = null, serverReady = true } = {}) {
  const map = new Map();
  for (const entry of entries ?? []) {
    if (entry.status === 'notAdded' && (!serverReady || pullingPaths?.has(entry.path))) continue;
    const chipValue = chipFor(entry);
    if (chipValue) map.set(entry.path, chipValue);
  }
  return map;
}
