import type { NpcGroupId, ProjectNpc, ProjectNpcGroup } from '../types';
import { asNpcGroupId } from '../types/ids';

export const DEFAULT_NPC_GROUP_COLOR = '#6b7280';

export function normalizeHexColor(raw: unknown, fallback = DEFAULT_NPC_GROUP_COLOR): string {
  if (typeof raw !== 'string') return fallback;
  const s = raw.trim();
  if (/^#[0-9a-fA-F]{6}$/u.test(s)) return s.toLowerCase();
  if (/^#[0-9a-fA-F]{3}$/u.test(s)) {
    const r = s[1]!;
    const g = s[2]!;
    const b = s[3]!;
    return `#${r}${r}${g}${g}${b}${b}`.toLowerCase();
  }
  return fallback;
}

export function uniqueNpcGroupName(base: string, existing: string[], exceptId?: NpcGroupId | null): string {
  const root = base.trim() || 'Group';
  const used = new Set(
    existing
      .map((n, i) => ({ n: n.trim().toLowerCase(), i }))
      .filter(() => true)
      .map((x) => x.n),
  );
  // existing is just names; caller filters except
  void exceptId;
  if (!used.has(root.toLowerCase())) return root;
  let i = 2;
  while (used.has(`${root.toLowerCase()} (${String(i)})`)) i += 1;
  return `${root} (${String(i)})`;
}

export function normalizeNpcGroups(raw: unknown): ProjectNpcGroup[] {
  if (!Array.isArray(raw)) return [];
  const parsed: ProjectNpcGroup[] = [];
  const ids = new Set<string>();
  for (const item of raw) {
    if (!item || typeof item !== 'object') continue;
    const obj = item as { id?: string; name?: string; color?: string; parentId?: string | null };
    if (!obj.id || typeof obj.name !== 'string') continue;
    const name = obj.name.trim();
    if (!name) continue;
    const id = String(obj.id);
    if (ids.has(id)) continue;
    ids.add(id);
    parsed.push({
      id: asNpcGroupId(id),
      name,
      color: normalizeHexColor(obj.color),
      parentId:
        typeof obj.parentId === 'string' && obj.parentId.trim() ? asNpcGroupId(obj.parentId.trim()) : null,
    });
  }
  // Drop parent links to missing groups (flatten to root).
  const idSet = new Set(parsed.map((g) => g.id));
  return parsed.map((g) => ({
    ...g,
    parentId: g.parentId && idSet.has(g.parentId) && g.parentId !== g.id ? g.parentId : null,
  }));
}

/** Разрешить groupId НПС; неизвестный → null («Без группы»). */
export function resolveNpcGroupId(raw: unknown, groupIds: Set<NpcGroupId>): NpcGroupId | null {
  if (typeof raw !== 'string' || !raw.trim()) return null;
  const id = asNpcGroupId(raw.trim());
  return groupIds.has(id) ? id : null;
}

export type NpcGroupTreeNode = {
  group: ProjectNpcGroup;
  children: NpcGroupTreeNode[];
  npcs: ProjectNpc[];
};

/** Дерево групп + НПС без группы отдельно. */
export function buildNpcGroupForest(
  groups: ProjectNpcGroup[],
  npcs: ProjectNpc[],
): { roots: NpcGroupTreeNode[]; ungrouped: ProjectNpc[] } {
  const byParent = new Map<string | null, ProjectNpcGroup[]>();
  for (const g of groups) {
    const key = g.parentId;
    const list = byParent.get(key) ?? [];
    list.push(g);
    byParent.set(key, list);
  }
  // Preserve array order among siblings (already ordered in groups array).
  const build = (parentId: NpcGroupId | null): NpcGroupTreeNode[] => {
    const siblings = groups.filter((g) => g.parentId === parentId);
    return siblings.map((group) => ({
      group,
      children: build(group.id),
      npcs: npcs.filter((n) => n.groupId === group.id),
    }));
  };
  return {
    roots: build(null),
    ungrouped: npcs.filter((n) => n.groupId === null),
  };
}

export function collectDescendantGroupIds(groups: ProjectNpcGroup[], rootId: NpcGroupId): Set<NpcGroupId> {
  const out = new Set<NpcGroupId>([rootId]);
  let changed = true;
  while (changed) {
    changed = false;
    for (const g of groups) {
      if (g.parentId && out.has(g.parentId) && !out.has(g.id)) {
        out.add(g.id);
        changed = true;
      }
    }
  }
  return out;
}

/** Нельзя сделать parentId потомком (цикл). */
export function wouldCreateGroupCycle(
  groups: ProjectNpcGroup[],
  groupId: NpcGroupId,
  newParentId: NpcGroupId | null,
): boolean {
  if (!newParentId) return false;
  if (newParentId === groupId) return true;
  return collectDescendantGroupIds(groups, groupId).has(newParentId);
}
