/**
 * @file splitDocumentNames.ts - Utilities for generating and sorting split document names.
 * When a document is split, the resulting fragments receive hierarchical names following the pattern "basename-parentId.split-N.ext", where N is an incrementing number. Nested splits append additional ".split-N" segments.
 * The sorting utility arranges document nodes so that split children appear immediately after their parent document, preserving the hierarchical relationship in the folder tree display.
 */

import { type DocumentNode } from '../components/Processing/DetailsFolderPanel/sections/FolderSection/FolderTree.tsx';
import { type Document } from '../api/document.ts';

/**
 * Escapes regex special characters in a string for safe use in RegExp constructors.
 *
 * @param string Input string to escape
 * @returns Escaped string safe for regex patterns
 */
function escapeRegExp(string: string) {
  return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

/**
 * Determines the next split document name using a hierarchical ".split-N" structure appended before the file extension.
 * In the case of a parent document (not split before), the document's ID is added to the new name.
 *
 * @param parentDocument Document being split
 * @param documents All work item documents
 * @returns New split name
 */
export function getNextSplitName(
  parentDocument: Document,
  documents: Document[]
): string {
  const parentName = parentDocument.documentName;

  const extIndex = parentName.lastIndexOf('.');
  const extension = extIndex === -1 ? '' : parentName.slice(extIndex);
  const baseName = extIndex === -1 ? parentName : parentName.slice(0, extIndex);

  const splitPattern = /\.split-\d+(?=\.[^.]+$)/;
  const hasSplit = splitPattern.test(parentName);

  if (!hasSplit) {
    // No existing ".split": new base is base-parentId
    const newBase = `${baseName}-${parentDocument.documentId}`;
    const regex = new RegExp(
      `^${escapeRegExp(newBase)}\\.split-(\\d+)${escapeRegExp(extension)}$`
    );

    const siblings = documents
      .map((document) => document.documentName)
      .filter((name) => regex.test(name));

    const usedNumbers = siblings.map((name) => {
      const m = name.match(/\.split-(\d+)(?=\.[^.]+$)/);
      return m ? parseInt(m[1], 10) : 0;
    });

    let nextNum = 1;
    while (usedNumbers.includes(nextNum)) nextNum++;

    return `${newBase}.split-${nextNum}${extension}`;
  } else {
    // Parent has ".split" in it already: just append new split
    const parentBase = baseName;
    const regex = new RegExp(
      `^${escapeRegExp(parentBase)}\\.split-(\\d+)${escapeRegExp(extension)}$`
    );

    const children = documents
      .map((document) => document.documentName)
      .filter((name) => regex.test(name));

    const usedNumbers = children.map((name) => {
      const m = name.match(/\.split-(\d+)(?=\.[^.]+$)/);
      return m ? parseInt(m[1], 10) : 0;
    });

    let nextNum = 1;
    while (usedNumbers.includes(nextNum)) nextNum++;

    return `${parentBase}.split-${nextNum}${extension}`;
  }
}

/**
 * Sorts FolderTree document nodes by ID, but with documents containing the ".split" substring grouped together in a hierarchy.
 *
 * @param documentNodes FolderTree document nodes
 * @returns Sorted document nodes
 */
export function sortDocumentNodesWithSplitHierarchy(
  documentNodes: DocumentNode[]
): DocumentNode[] {
  const nodes = [...documentNodes];
  const splitString = '.split-';

  /**
   * Parsed components of a split document name for sorting purposes.
   */
  type ParsedDocumentName = {
    /** The base file name without split segments or extension */
    baseName: string;

    /** The original parent document ID extracted from the first split segment */
    parentId?: number;

    /** Numeric split sequence path (e.g., [1, 2] for ".split-1.split-2") */
    sequence: number[];

    /** File extension without the leading dot */
    extension: string;
  };

  // Parses a document's name
  function parseName(name: string): ParsedDocumentName {
    const lastDotIndex = name.lastIndexOf('.');
    const fileName = lastDotIndex ? name.slice(0, lastDotIndex) : name;
    const extension = lastDotIndex ? name.slice(lastDotIndex + 1) : '';

    const splitParts = fileName.split(splitString);
    // CASE: Not a split document
    if (splitParts.length === 1) {
      return {
        baseName: fileName,
        parentId: undefined,
        sequence: [],
        extension,
      };
    }

    // First split part is `<baseName>-<topParentId>`
    const firstSplitPart = splitParts[0];
    let baseName = firstSplitPart;
    let parentId: number | undefined = undefined;
    const lastDashIndex = firstSplitPart.lastIndexOf('-');
    if (lastDashIndex) {
      const idString = firstSplitPart.slice(lastDashIndex + 1);
      if (/^\d+$/.test(idString)) {
        parentId = parseInt(idString, 10);
        baseName = firstSplitPart.slice(0, lastDashIndex);
      }
    }

    const sequence = splitParts.slice(1).map((s) => {
      const n = parseInt(s, 10);
      return isNaN(n) ? 0 : n;
    });

    return {
      baseName,
      parentId,
      sequence,
      extension,
    };
  }

  // Quick lookup ID -> node
  const idToNode = new Map<number, DocumentNode>();
  for (const node of nodes) {
    idToNode.set(node.documentId, node);
  }

  // Separator arrays
  const nonSplit = nodes.filter(
    (node) => !node.documentName.includes(splitString)
  );
  const splitOnly = nodes.filter((node) =>
    node.documentName.includes(splitString)
  );

  // Group split nodes by top origin: `${baseName}-${topParentId}.${ext}` or `{baseName}.orph.${ext}` if parentId missing
  const groups = new Map<string, DocumentNode[]>();
  const groupMeta = new Map<string, Partial<ParsedDocumentName>>();
  for (const node of splitOnly) {
    const parsed = parseName(node.documentName);
    const key =
      parsed.parentId !== undefined
        ? `${parsed.baseName}-${parsed.parentId}.${parsed.extension}`
        : `${parsed.baseName}.orph.${parsed.extension}`;
    if (!groups.has(key)) {
      groups.set(key, []);
    }
    groups.get(key)!.push(node);
    if (!groupMeta.has(key)) {
      groupMeta.set(key, {
        baseName: parsed.baseName,
        parentId: parsed.parentId,
        extension: parsed.extension,
      });
    }
  }

  /**
   * A single node in the split hierarchy tree used for sorting.
   */
  type Group = {
    document: DocumentNode;
    sequence: number[];
    children: any[];
    parentSequenceKey?: string;
  };
  /**
   * A tree structure for a family of split documents sharing the same origin.
   */
  type GroupTree = {
    nodesBySequence: Map<string, Group>;

    /** Maps top-level split index to the root Group nodes at that index */
    topIndexMap: Map<number, Partial<Group>[]>;

    /** Whether the original parent document still exists in the document list */
    parentExists: boolean;

    parentId?: number;
  };

  // Build per-group trees keyed by sequence (e.g., '1', '1.2', '2')
  const groupTrees = new Map<string, GroupTree>();
  for (const [key, list] of groups.entries()) {
    const meta = groupMeta.get(key)!;
    const nodesBySequence = new Map<string, Group>();

    // Create node entries by their sequence key
    for (const document of list) {
      const parsed = parseName(document.documentName);
      const sequenceKey = parsed.sequence.join('.');
      nodesBySequence.set(sequenceKey, {
        document,
        sequence: parsed.sequence.slice(),
        children: [],
        parentSequenceKey: undefined,
      });
    }

    // Attach children to parents where the parent sequence exists in this group
    // @ts-expect-error Unused map key
    for (const [sequenceKey, nodeObj] of nodesBySequence.entries()) {
      if (nodeObj.sequence.length > 1) {
        const parentSequence = nodeObj.sequence.slice(0, -1).join('.');
        if (nodesBySequence.has(parentSequence)) {
          nodesBySequence.get(parentSequence)!.children.push(nodeObj);
          nodeObj.parentSequenceKey = parentSequence;
        } else {
          // Parent sequence not present -> treat as root in this group
          nodeObj.parentSequenceKey = undefined;
        }
      } else {
        // Top-level split (sequence length 1) -> root in group unless parent document attaches it
        nodeObj.parentSequenceKey = undefined;
      }
    }

    // Collect roots and bucket them by their top-level split index
    const topIndexMap = new Map<number, Partial<Group>[]>();
    for (const nodeObj of nodesBySequence.values()) {
      if (nodeObj.parentSequenceKey === undefined) {
        const top = nodeObj.sequence[0] ?? 0;
        const array = topIndexMap.get(top) ?? [];
        array.push(nodeObj);
        topIndexMap.set(top, array);
      }
    }

    const parentExists =
      meta.parentId !== undefined && idToNode.has(meta.parentId);
    groupTrees.set(key, {
      nodesBySequence,
      topIndexMap,
      parentExists,
      parentId: meta.parentId,
    });
  }

  // Helper: compute subtree minimum documentId (cached)
  const minCache = new WeakMap<object, number>();
  function subtreeMin(group: Group): number {
    if (minCache.has(group as any)) {
      return minCache.get(group as any)!;
    }
    let min = group.document.documentId;
    for (const child of group.children as any[]) {
      const childMin = subtreeMin(child);
      if (childMin < min) {
        min = childMin;
      }
    }
    minCache.set(group as any, min);
    return min;
  }

  // Helper: compare split sequence arrays (level-by-level), shorter prefix precedes longer
  function compareSequence(a: number[], b: number[]): number {
    const minLength = Math.min(a.length, b.length);
    for (let i = 0; i < minLength; i++) {
      if (a[i] !== b[i]) {
        return a[i] - b[i];
      }
    }
    return a.length - b.length;
  }

  // Helper: flatten group (document then children), children ordered by last sequence element and tie-broken deterministically
  function flatten(group: Group, out: DocumentNode[]) {
    // Append this group
    out.push(group.document);
    // Sort children by immediate split index (last element), then by subtreeMin to be deterministic
    group.children.sort((a: Group, b: Group) => {
      const ai = a.sequence[a.sequence.length - 1] ?? 0;
      const bi = b.sequence[b.sequence.length - 1] ?? 0;
      if (ai !== bi) {
        return ai - bi;
      }
      return subtreeMin(a) - subtreeMin(b);
    });
    for (const child of group.children) {
      flatten(child, out);
    }
  }

  // Map parent original documentId -> group keys to attach under that parent
  const attachMap = new Map<number, string[]>();
  for (const [key, info] of groupTrees.entries()) {
    if (info.parentExists && info.parentId !== undefined) {
      const array = attachMap.get(info.parentId) ?? [];
      array.push(key);
      attachMap.set(info.parentId, array);
    }
  }

  // Build top-level entries:
  // - every non-split (original parent) document becomes an entry (we'll attach any groups under it)
  // - for orphaned groups, each top-level split-index bucket becomes an entry placed by that bucket's min documentID
  type Entry =
    | { kind: 'parent'; id: number; document: DocumentNode }
    | { kind: 'orphanBucket'; id: number; groupKey: string; topIndex: number };
  const entries: Entry[] = [];
  for (const node of nonSplit) {
    entries.push({
      kind: 'parent',
      id: node.documentId,
      document: node,
    });
  }
  // For orphaned groups, create an entry per topIndex bucket using the bucket's minimum documentId for placement
  for (const [key, info] of groupTrees.entries()) {
    if (!info.parentExists) {
      for (const [topIndex, roots] of info.topIndexMap.entries()) {
        // Bucket min ID
        const minId = Math.min(...roots.map((r) => subtreeMin(r as Group)));
        entries.push({
          kind: 'orphanBucket',
          id: minId,
          groupKey: key,
          topIndex,
        });
      }
    }
  }

  // Sort base entries by ID ascending
  entries.sort((a, b) => a.id - b.id);

  const result: DocumentNode[] = [];

  // Helper: process a specific group topIndex bucket (either under a parent or as an orphan entry)
  function processGroupBucket(key: string, topIndex: number) {
    const info = groupTrees.get(key)!;

    // If there is a node for the exact top-level sequence (e.g., sequenceKey === `${topIndex}`) then flatten that node (it may have children)
    const sequenceKey = String(topIndex);
    if (info.nodesBySequence.has(sequenceKey)) {
      const node = info.nodesBySequence.get(sequenceKey);
      flatten(node as Group, result);
      return;
    }

    // Otherwise we have deeper nodes for this topIndex (e.g., 1.1, 1.2) whose immediate parent (1) is missing
    // We'll sort them by their sequence (so 1.1 then 1.2, etc.) and flatten each
    const bucketRoots = info.topIndexMap.get(topIndex) ?? [];
    bucketRoots.sort((a, b) => {
      const compare = compareSequence(
        a.sequence as number[],
        b.sequence as number[]
      );
      if (compare !== 0) {
        return compare;
      }
      return subtreeMin(a as Group) - subtreeMin(b as Group);
    });
    for (const root of bucketRoots) {
      flatten(root as Group, result);
    }
  }

  // When attaching groups to a parent, process that group's top indices in natural numeric order
  function processGroupsAttachedToParent(parentDocumentId: number) {
    const keys = attachMap.get(parentDocumentId) ?? [];
    // Deterministic order between multiple attach groups: use the group's smallest bucket min ID
    keys.sort((a, b) => {
      const aInfo = groupTrees.get(a)!;
      const bInfo = groupTrees.get(b)!;
      const aMin = Math.min(
        ...Array.from(aInfo.topIndexMap.values())
          .flat()
          .map((r) => subtreeMin(r as Group))
      );
      const bMin = Math.min(
        ...Array.from(bInfo.topIndexMap.values())
          .flat()
          .map((r) => subtreeMin(r as Group))
      );
      return aMin - bMin;
    });

    for (const key of keys) {
      const info = groupTrees.get(key)!;
      // Order top-level indices in ascending numeric order when parent exists (hierarchy priority)
      const topIndices = Array.from(info.topIndexMap.keys()).sort(
        (a, b) => a - b
      );
      for (const topIndex of topIndices) {
        processGroupBucket(key, topIndex);
      }
    }
  }

  // Iterate top-level entries and build final result
  for (const entry of entries) {
    if (entry.kind === 'parent') {
      // Push the original parent document
      result.push(entry.document);

      // Then append any groups attached to this parent (preserving hierarchical split order)
      processGroupsAttachedToParent(entry.document.documentId);
    } else {
      // Orphan bucket: this is a topIndex bucket in an orphaned group, flatten it where it belongs
      processGroupBucket(entry.groupKey, entry.topIndex);
    }
  }

  // As a safety net, append any nodes not yet included
  const seen = new Set<number>(result.map((r) => r.documentId));
  for (const node of nodes) {
    if (!seen.has(node.documentId)) {
      result.push(node);
      seen.add(node.documentId);
    }
  }

  return result;
}
