const normalizeName = (name) => String(name || '').split(/[\\/]/).pop().trim().toLowerCase();
const getRelativePath = (file) => String(file?.webkitRelativePath || file?.name || '');
const getRelativeDirectory = (file) => {
  const parts = getRelativePath(file).split(/[\\/]/).filter(Boolean);
  parts.pop();
  return parts.join('/').toLowerCase();
};

const stripExtension = (name) => String(name || '').replace(/\.[^/.]+$/, '').trim();

const makeUniqueModelId = (baseId, usedIds) => {
  const normalizedBase = baseId || 'model';
  let candidate = normalizedBase;
  let suffix = 2;

  while (usedIds.has(candidate.toLowerCase())) {
    candidate = `${normalizedBase}-${suffix}`;
    suffix += 1;
  }

  usedIds.add(candidate.toLowerCase());
  return candidate;
};

const deriveModelId = (objFile) => {
  return stripExtension(objFile?.name);
};

const findFileByName = (files, targetName, preferredDirectory) => {
  const normalizedTarget = normalizeName(targetName);
  if (!normalizedTarget) return null;

  const candidates = files.filter((file) => normalizeName(file.name) === normalizedTarget);
  if (candidates.length === 0) return null;

  return candidates.find((file) => getRelativeDirectory(file) === preferredDirectory) || candidates[0];
};

const decorateDroppedFile = (file, relativePath) => {
  try {
    Object.defineProperty(file, 'webkitRelativePath', {
      value: relativePath,
      configurable: true,
    });
    return file;
  } catch {
    return Object.assign(file, { webkitRelativePath: relativePath });
  }
};

const readDirectoryEntries = (reader) => new Promise((resolve, reject) => {
  const collected = [];

  const readBatch = () => {
    reader.readEntries((entries) => {
      if (!entries.length) {
        resolve(collected);
        return;
      }

      collected.push(...entries);
      readBatch();
    }, reject);
  };

  readBatch();
});

const readDroppedEntry = async (entry, parentPath = '') => {
  if (!entry) return [];

  if (entry.isFile) {
    const file = await new Promise((resolve, reject) => entry.file(resolve, reject));
    return [decorateDroppedFile(file, `${parentPath}${file.name}`)];
  }

  if (entry.isDirectory) {
    const reader = entry.createReader();
    const entries = await readDirectoryEntries(reader);
    const nested = await Promise.all(entries.map((child) => readDroppedEntry(child, `${parentPath}${entry.name}/`)));
    return nested.flat();
  }

  return [];
};

const parseObjMtllibNames = (objContent) => {
  const names = [];
  const regex = /^\s*mtllib\s+(.+)$/gim;
  let match;

  while ((match = regex.exec(objContent)) !== null) {
    const tokens = match[1]
      .trim()
      .split(/\s+/)
      .map(normalizeName)
      .filter(Boolean);

    names.push(...tokens);
  }

  return Array.from(new Set(names));
};

const parseMtlTextureNames = (mtlContent) => {
  const names = [];
  const textureLineRegex = /^\s*(map_Kd|map_Ka|map_Ks|map_d|bump|map_Bump|disp|decal|refl)\s+(.+)$/gim;
  let match;

  while ((match = textureLineRegex.exec(mtlContent)) !== null) {
    const rhs = match[2].trim();
    if (!rhs) continue;

    const tokens = rhs.split(/\s+/).filter(Boolean);
    const candidate = tokens[tokens.length - 1];
    const normalized = normalizeName(candidate);
    if (normalized) names.push(normalized);
  }

  return Array.from(new Set(names));
};

export async function buildModelUploadGroups(files) {
  const selection = Array.from(files || []);
  const objFiles = selection.filter((file) => file.name.toLowerCase().endsWith('.obj'));
  const mtlFiles = selection.filter((file) => file.name.toLowerCase().endsWith('.mtl'));
  const textureFiles = selection.filter((file) => file.name.toLowerCase().match(/\.(jpg|jpeg|png)$/i));
  const usedIds = new Set();
  const groups = [];

  for (const objFile of objFiles) {
    const objDirectory = getRelativeDirectory(objFile);
    const objContent = await objFile.text();
    const mtllibNames = parseObjMtllibNames(objContent);
    const mtlFile = mtllibNames
      .map((name) => findFileByName(mtlFiles, name, objDirectory))
      .find(Boolean) || null;

    let textureFile = null;
    if (mtlFile) {
      const mtlContent = await mtlFile.text();
      const textureNames = parseMtlTextureNames(mtlContent);
      const mtlDirectory = getRelativeDirectory(mtlFile);
      textureFile = textureNames
        .map((name) => findFileByName(textureFiles, name, mtlDirectory))
        .find(Boolean) || null;
    }

    groups.push({
      modelId: makeUniqueModelId(deriveModelId(objFile), usedIds),
      obj: objFile,
      mtl: mtlFile,
      texture: textureFile,
    });
  }

  return groups;
}

export async function extractDroppedModelFiles(event) {
  const items = Array.from(event?.dataTransfer?.items || []);

  if (items.length > 0 && items.some((item) => typeof item.webkitGetAsEntry === 'function')) {
    const nestedFiles = await Promise.all(
      items
        .filter((item) => item.kind === 'file')
        .map((item) => readDroppedEntry(item.webkitGetAsEntry()))
    );

    return nestedFiles.flat();
  }

  return Array.from(event?.dataTransfer?.files || []);
}

export async function validateModelFileBinding({ objFile, mtlFile, textureFile }) {
  if (!objFile && (mtlFile || textureFile)) {
    return { isValid: false, messageKey: 'missingObjFile' };
  }

  if (!objFile) {
    return { isValid: true };
  }

  if (!mtlFile || !textureFile) {
    return { isValid: false, messageKey: 'requireCompleteModelSet' };
  }

  if (textureFile && !mtlFile) {
    return { isValid: false, messageKey: 'textureRequiresMtl' };
  }

  if (mtlFile) {
    const objContent = await objFile.text();
    const mtllibNames = parseObjMtllibNames(objContent);
    const normalizedMtlName = normalizeName(mtlFile.name);

    if (mtllibNames.length === 0) {
      return { isValid: false, messageKey: 'objMissingMtlBinding' };
    }

    if (!mtllibNames.includes(normalizedMtlName)) {
      return { isValid: false, messageKey: 'mtlNotMatchingObj' };
    }
  }

  if (textureFile && mtlFile) {
    const mtlContent = await mtlFile.text();
    const textureNames = parseMtlTextureNames(mtlContent);
    const normalizedTextureName = normalizeName(textureFile.name);

    if (textureNames.length === 0) {
      return { isValid: false, messageKey: 'mtlMissingTextureBinding' };
    }

    if (!textureNames.includes(normalizedTextureName)) {
      return { isValid: false, messageKey: 'textureNotMatchingMtl' };
    }
  }

  return { isValid: true };
}
