'use strict';

const fs = require('fs');
const path = require('path');

const { defineModel } = require('./define-model');

function loadSchemaPath(inputPath) {
  const absPath = path.resolve(inputPath);

  if (!fs.existsSync(absPath)) {
    throw new Error(`Schema path not found: ${absPath}`);
  }

  const stat = fs.statSync(absPath);
  if (stat.isFile()) {
    if (path.extname(absPath) !== '.js') {
      throw new Error(`Schema file must have .js extension: ${absPath}`);
    }
    return loadSchemaFiles([absPath]);
  }
  if (stat.isDirectory()) {
    return loadSchemaFolder(absPath);
  }
  throw new Error(`Schema path must be a file or directory: ${absPath}`);
}

function loadSchemaFolder(folderPath) {
  const absPath = path.resolve(folderPath);

  if (!fs.existsSync(absPath) || !fs.statSync(absPath).isDirectory()) {
    throw new Error(`Schema folder not found: ${absPath}`);
  }

  const filePaths = collectSchemaFiles(absPath).sort();
  return loadSchemaFiles(filePaths, absPath);
}

function collectSchemaFiles(absPath) {
  const result = [];
  const entries = fs.readdirSync(absPath, { withFileTypes: true });
  for (const entry of entries) {
    const entryPath = path.join(absPath, entry.name);
    if (entry.isFile()) {
      if (path.extname(entry.name) === '.js') result.push(entryPath);
      continue;
    }
    if (entry.isDirectory()) {
      // Skip hidden folders (.git, .vscode) and node_modules to avoid
      // accidentally loading dependencies or VCS metadata as schema files.
      if (entry.name.startsWith('.')) continue;
      if (entry.name === 'node_modules') continue;
      result.push(...collectSchemaFiles(entryPath));
    }
  }
  return result;
}

function loadSchemaFiles(filePaths, rootFolder) {
  const models = new Map();
  const qualifiedNameToFile = new Map();

  for (const entryPath of filePaths) {
    const entry = displayPath(entryPath, rootFolder);

    // Clear cache to support re-load (watcher / repeated test scenario).
    delete require.cache[entryPath];

    let factory;
    try {
      factory = require(entryPath);
    } catch (err) {
      throw new Error(`Failed to load schema file '${entry}': ${err.message}`);
    }

    if (typeof factory !== 'function') {
      throw new Error(
        `Schema file '${entry}' must export a factory function. ` +
        `Use: module.exports = ({ defineModel }) => defineModel(...)`
      );
    }

    let model;
    try {
      model = factory({ defineModel });
    } catch (err) {
      throw new Error(`Failed to load schema file '${entry}': ${err.message}`);
    }

    if (!isPlainObject(model) || typeof model.tableName !== 'string') {
      throw new Error(
        `Schema file '${entry}' factory must return a defineModel() result, got ${describeType(model)}`
      );
    }

    const qualified = model.qualifiedName || model.tableName;

    if (qualifiedNameToFile.has(qualified)) {
      const previousFile = qualifiedNameToFile.get(qualified);
      throw new Error(
        `Duplicate table name '${qualified}' in files: ${previousFile}, ${entry}`
      );
    }

    // Attach absolute file path so downstream validators can populate Issue.file.
    // Non-enumerable to avoid surprising JSON.stringify() callers.
    Object.defineProperty(model, '_filePath', {
      value: entryPath,
      enumerable: false,
      writable: false,
      configurable: false
    });

    models.set(qualified, model);
    qualifiedNameToFile.set(qualified, entry);
  }

  return models;
}

function displayPath(entryPath, rootFolder) {
  if (!rootFolder) return path.basename(entryPath);
  // Normalise to forward-slash so error messages are stable across OS.
  const rel = path.relative(rootFolder, entryPath);
  return rel.split(path.sep).join('/');
}

function isPlainObject(v) {
  return v !== null && typeof v === 'object' && !Array.isArray(v);
}

function describeType(v) {
  if (v === null) return 'null';
  if (Array.isArray(v)) return 'array';
  return typeof v;
}

module.exports = { loadSchemaPath, loadSchemaFolder };
