import { queryWithRLS, queryOneWithRLS, mutateWithRLS } from "../db.js";
import { entityRegistry } from "../entities/registry.js";
import { executeBeforeHooks, executeAfterHooks } from "../entities/hooks.js";
const SYSTEM_FIELDS = ["id", "userId", "teamId", "createdAt", "updatedAt"];
const DEFAULT_LIMIT = 20;
const DEFAULT_ORDER_BY = "createdAt";
const DEFAULT_ORDER_DIR = "desc";
const SAFE_SQL_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
function validateSqlIdentifier(name, type) {
  if (!name || !SAFE_SQL_IDENTIFIER.test(name)) {
    throw new Error(`Invalid ${type} name: "${name}" contains unsafe characters`);
  }
}
function getTableName(entityConfig) {
  const tableName = entityConfig.tableName || entityConfig.slug;
  validateSqlIdentifier(tableName, "table");
  return tableName;
}
function getFieldNames(entityConfig) {
  if (!entityConfig.fields || !Array.isArray(entityConfig.fields)) {
    return [];
  }
  return entityConfig.fields.map((field) => {
    validateSqlIdentifier(field.name, "field");
    return field.name;
  });
}
function getSearchableFields(entityConfig) {
  if (!entityConfig.fields || !Array.isArray(entityConfig.fields)) {
    return [];
  }
  return entityConfig.fields.filter((field) => {
    var _a;
    return ((_a = field.api) == null ? void 0 : _a.searchable) === true;
  }).map((field) => field.name);
}
function quoteField(field) {
  return field.includes("_") || /[A-Z]/.test(field) ? `"${field}"` : field;
}
function buildSelectClause(fieldNames) {
  const allFields = [...SYSTEM_FIELDS, ...fieldNames];
  return allFields.map((f) => quoteField(f)).join(", ");
}
function buildWhereClause(where, validFields, startIndex = 1) {
  const conditions = [];
  const params = [];
  let paramIndex = startIndex;
  for (const [key, value] of Object.entries(where)) {
    if (value === void 0) continue;
    if (!validFields.includes(key)) {
      throw new Error(`Invalid filter field: "${key}"`);
    }
    const quotedKey = quoteField(key);
    if (value === null) {
      conditions.push(`${quotedKey} IS NULL`);
    } else if (Array.isArray(value)) {
      conditions.push(`${quotedKey} = ANY($${paramIndex})`);
      params.push(value);
      paramIndex++;
    } else {
      conditions.push(`${quotedKey} = $${paramIndex}`);
      params.push(value);
      paramIndex++;
    }
  }
  return {
    clause: conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "",
    params,
    nextIndex: paramIndex
  };
}
function buildSearchCondition(search, searchableFields, paramIndex) {
  if (!search || searchableFields.length === 0) {
    return null;
  }
  const searchConditions = searchableFields.map((f) => `${quoteField(f)} ILIKE $${paramIndex}`).join(" OR ");
  return {
    condition: `(${searchConditions})`,
    param: `%${search}%`
  };
}
function mapRowToEntity(row) {
  const entity = {};
  for (const [key, value] of Object.entries(row)) {
    entity[key] = value === null ? void 0 : value;
  }
  return entity;
}
function validateFieldType(field, value) {
  switch (field.type) {
    case "text":
    case "textarea":
    case "email":
    case "url":
    case "markdown":
    case "richtext":
    case "code":
    case "phone":
      if (typeof value !== "string") {
        return `Field "${field.name}" must be a string`;
      }
      break;
    case "number":
    case "range":
    case "rating":
      if (typeof value !== "number") {
        return `Field "${field.name}" must be a number`;
      }
      break;
    case "boolean":
      if (typeof value !== "boolean") {
        return `Field "${field.name}" must be a boolean`;
      }
      break;
    case "select":
    case "radio":
    case "buttongroup":
      if (field.options && !field.options.some((opt) => opt.value === value)) {
        return `Field "${field.name}" has invalid option value`;
      }
      break;
    case "multiselect":
    case "tags":
      if (!Array.isArray(value)) {
        return `Field "${field.name}" must be an array`;
      }
      if (field.options) {
        const invalidValues = value.filter(
          (v) => !field.options.some((opt) => opt.value === v)
        );
        if (invalidValues.length > 0) {
          return `Field "${field.name}" contains invalid option values`;
        }
      }
      break;
    case "date":
    case "datetime":
      if (typeof value !== "string" && !(value instanceof Date)) {
        return `Field "${field.name}" must be a date string or Date object`;
      }
      break;
    case "json":
      break;
  }
  return null;
}
function validateEntityData(entityConfig, data, isUpdate = false) {
  const errors = [];
  for (const field of entityConfig.fields) {
    const value = data[field.name];
    if (!isUpdate && field.required && (value === void 0 || value === null || value === "")) {
      errors.push(`Field "${field.name}" is required`);
      continue;
    }
    if (value === void 0 || value === null) {
      continue;
    }
    if (field.validation) {
      try {
        const result = field.validation.safeParse(value);
        if (!result.success) {
          const zodError = result.error.issues[0];
          errors.push(`Field "${field.name}": ${(zodError == null ? void 0 : zodError.message) || "Invalid value"}`);
          continue;
        }
      } catch {
      }
    }
    const typeError = validateFieldType(field, value);
    if (typeError) {
      errors.push(typeError);
    }
  }
  return { valid: errors.length === 0, errors };
}
function createHookContext(entitySlug, operation, userId, data, previousData, role = "member") {
  return {
    entityName: entitySlug,
    operation,
    data,
    previousData,
    user: { id: userId, role }
  };
}
class GenericEntityService {
  /**
   * Get entity by ID
   *
   * @param entitySlug - The entity type slug (e.g., 'campaigns', 'schools')
   * @param id - Entity ID
   * @param userId - Current user ID for RLS
   * @param teamId - Team ID for team isolation (prevents cross-team access)
   * @returns Entity or null if not found
   */
  static async getById(entitySlug, id, userId, teamId) {
    if (!(id == null ? void 0 : id.trim())) {
      throw new Error("Entity ID is required");
    }
    if (!(userId == null ? void 0 : userId.trim())) {
      throw new Error("User ID is required for authentication");
    }
    const entityConfig = entityRegistry.get(entitySlug);
    if (!entityConfig) {
      throw new Error(`Entity "${entitySlug}" not found in registry`);
    }
    const tableName = getTableName(entityConfig);
    const fieldNames = getFieldNames(entityConfig);
    const selectClause = buildSelectClause(fieldNames);
    const whereClause = teamId ? `WHERE id = $1 AND "teamId" = $2` : `WHERE id = $1`;
    const params = teamId ? [id, teamId] : [id];
    const row = await queryOneWithRLS(
      `SELECT ${selectClause} FROM ${tableName} ${whereClause}`,
      params,
      userId
    );
    return row ? mapRowToEntity(row) : null;
  }
  /**
   * List entities with pagination, filtering, and search
   *
   * @param entitySlug - The entity type slug
   * @param userId - Current user ID for RLS
   * @param options - List options (limit, offset, where, search, orderBy)
   * @returns Paginated result with data and total count
   */
  static async list(entitySlug, userId, options = {}) {
    var _a;
    if (!(userId == null ? void 0 : userId.trim())) {
      throw new Error("User ID is required for authentication");
    }
    const entityConfig = entityRegistry.get(entitySlug);
    if (!entityConfig) {
      throw new Error(`Entity "${entitySlug}" not found in registry`);
    }
    const tableName = getTableName(entityConfig);
    const fieldNames = getFieldNames(entityConfig);
    const searchableFields = getSearchableFields(entityConfig);
    const selectClause = buildSelectClause(fieldNames);
    const allFields = [...SYSTEM_FIELDS, ...fieldNames];
    const {
      limit = DEFAULT_LIMIT,
      offset = 0,
      orderBy = DEFAULT_ORDER_BY,
      orderDir = DEFAULT_ORDER_DIR,
      where = {},
      search,
      teamId
    } = options;
    const effectiveWhere = teamId ? { ...where, teamId } : where;
    const { clause: whereClause, params, nextIndex } = buildWhereClause(effectiveWhere, allFields);
    let fullWhereClause = whereClause;
    const allParams = [...params];
    let paramIdx = nextIndex;
    if (search) {
      const searchResult = buildSearchCondition(search, searchableFields, paramIdx);
      if (searchResult) {
        const connector = whereClause ? " AND " : "WHERE ";
        fullWhereClause = `${whereClause}${connector}${searchResult.condition}`;
        allParams.push(searchResult.param);
        paramIdx++;
      }
    }
    const validOrderBy = allFields.includes(orderBy) ? orderBy : DEFAULT_ORDER_BY;
    const validOrderDir = orderDir === "asc" ? "ASC" : "DESC";
    const orderColumn = quoteField(validOrderBy);
    const countResult = await queryWithRLS(
      `SELECT COUNT(*)::text as count FROM ${tableName} ${fullWhereClause}`,
      allParams,
      userId
    );
    const total = parseInt(((_a = countResult[0]) == null ? void 0 : _a.count) || "0", 10);
    const limitIdx = paramIdx;
    const offsetIdx = paramIdx + 1;
    const rows = await queryWithRLS(
      `SELECT ${selectClause}
       FROM ${tableName}
       ${fullWhereClause}
       ORDER BY ${orderColumn} ${validOrderDir}
       LIMIT $${limitIdx} OFFSET $${offsetIdx}`,
      [...allParams, limit, offset],
      userId
    );
    return {
      data: rows.map((row) => mapRowToEntity(row)),
      total,
      limit,
      offset
    };
  }
  /**
   * Create a new entity
   *
   * @param entitySlug - The entity type slug
   * @param userId - Current user ID for RLS and ownership
   * @param teamId - Team ID for team isolation
   * @param data - Entity data
   * @returns Created entity
   */
  static async create(entitySlug, userId, teamId, data) {
    if (!(userId == null ? void 0 : userId.trim())) {
      throw new Error("User ID is required");
    }
    if (!(teamId == null ? void 0 : teamId.trim())) {
      throw new Error("Team ID is required");
    }
    const entityConfig = entityRegistry.get(entitySlug);
    if (!entityConfig) {
      throw new Error(`Entity "${entitySlug}" not found in registry`);
    }
    const beforeContext = createHookContext(entitySlug, "create", userId, data);
    const beforeResult = await executeBeforeHooks(entitySlug, "create", beforeContext);
    if (!beforeResult.continue) {
      throw new Error(beforeResult.error || "Operation cancelled by hook");
    }
    const finalData = beforeResult.data ?? data;
    const validation = validateEntityData(entityConfig, finalData, false);
    if (!validation.valid) {
      throw new Error(`Validation failed: ${validation.errors.join(", ")}`);
    }
    const tableName = getTableName(entityConfig);
    const fieldNames = getFieldNames(entityConfig);
    const selectClause = buildSelectClause(fieldNames);
    const fields = ["userId", "teamId"];
    const values = [userId, teamId];
    const placeholders = ["$1", "$2"];
    let paramIndex = 3;
    for (const field of fieldNames) {
      if (field in finalData && finalData[field] !== void 0) {
        fields.push(field);
        values.push(finalData[field]);
        placeholders.push(`$${paramIndex}`);
        paramIndex++;
      }
    }
    const quotedFields = fields.map((f) => quoteField(f)).join(", ");
    const result = await mutateWithRLS(
      `INSERT INTO ${tableName} (${quotedFields})
       VALUES (${placeholders.join(", ")})
       RETURNING ${selectClause}`,
      values,
      userId
    );
    if (!result.rows[0]) {
      throw new Error("Failed to create entity");
    }
    const createdEntity = mapRowToEntity(result.rows[0]);
    const afterContext = createHookContext(entitySlug, "create", userId, createdEntity);
    executeAfterHooks(entitySlug, "create", afterContext).catch((err) => {
      console.error(`[GenericEntityService] afterCreate hook error for ${entitySlug}:`, err);
    });
    return createdEntity;
  }
  /**
   * Update an existing entity
   *
   * @param entitySlug - The entity type slug
   * @param id - Entity ID
   * @param userId - Current user ID for RLS
   * @param data - Fields to update
   * @param teamId - Team ID for team isolation (prevents cross-team access)
   * @returns Updated entity
   */
  static async update(entitySlug, id, userId, data, teamId) {
    if (!(id == null ? void 0 : id.trim())) {
      throw new Error("Entity ID is required");
    }
    if (!(userId == null ? void 0 : userId.trim())) {
      throw new Error("User ID is required");
    }
    const entityConfig = entityRegistry.get(entitySlug);
    if (!entityConfig) {
      throw new Error(`Entity "${entitySlug}" not found in registry`);
    }
    const tableName = getTableName(entityConfig);
    const fieldNames = getFieldNames(entityConfig);
    const selectClause = buildSelectClause(fieldNames);
    const fetchWhereClause = teamId ? `WHERE id = $1 AND "teamId" = $2` : `WHERE id = $1`;
    const fetchParams = teamId ? [id, teamId] : [id];
    const currentEntity = await queryOneWithRLS(
      `SELECT ${selectClause} FROM ${tableName} ${fetchWhereClause}`,
      fetchParams,
      userId
    );
    if (!currentEntity) {
      throw new Error("Entity not found or not authorized");
    }
    const beforeContext = createHookContext(entitySlug, "update", userId, data, currentEntity);
    const beforeResult = await executeBeforeHooks(entitySlug, "update", beforeContext);
    if (!beforeResult.continue) {
      throw new Error(beforeResult.error || "Operation cancelled by hook");
    }
    const finalData = beforeResult.data ?? data;
    const validation = validateEntityData(entityConfig, finalData, true);
    if (!validation.valid) {
      throw new Error(`Validation failed: ${validation.errors.join(", ")}`);
    }
    const setClauses = [];
    const values = [];
    let paramIndex = 1;
    for (const field of fieldNames) {
      if (field in finalData) {
        setClauses.push(`${quoteField(field)} = $${paramIndex}`);
        values.push(finalData[field]);
        paramIndex++;
      }
    }
    if (setClauses.length === 0) {
      throw new Error("No fields to update");
    }
    setClauses.push(`"updatedAt" = now()`);
    values.push(id);
    const idParamIndex = paramIndex;
    paramIndex++;
    let updateWhereClause = `WHERE id = $${idParamIndex}`;
    if (teamId) {
      values.push(teamId);
      updateWhereClause += ` AND "teamId" = $${paramIndex}`;
    }
    const result = await mutateWithRLS(
      `UPDATE ${tableName}
       SET ${setClauses.join(", ")}
       ${updateWhereClause}
       RETURNING ${selectClause}`,
      values,
      userId
    );
    if (!result.rows[0]) {
      throw new Error("Update failed");
    }
    const updatedEntity = mapRowToEntity(result.rows[0]);
    const afterContext = createHookContext(entitySlug, "update", userId, updatedEntity, currentEntity);
    executeAfterHooks(entitySlug, "update", afterContext).catch((err) => {
      console.error(`[GenericEntityService] afterUpdate hook error for ${entitySlug}:`, err);
    });
    return updatedEntity;
  }
  /**
   * Delete an entity
   *
   * @param entitySlug - The entity type slug
   * @param id - Entity ID
   * @param userId - Current user ID for RLS
   * @param teamId - Team ID for team isolation (prevents cross-team access)
   * @returns true if deleted, false if not found
   */
  static async delete(entitySlug, id, userId, teamId) {
    if (!(id == null ? void 0 : id.trim())) {
      throw new Error("Entity ID is required");
    }
    if (!(userId == null ? void 0 : userId.trim())) {
      throw new Error("User ID is required");
    }
    const entityConfig = entityRegistry.get(entitySlug);
    if (!entityConfig) {
      throw new Error(`Entity "${entitySlug}" not found in registry`);
    }
    const tableName = getTableName(entityConfig);
    const fieldNames = getFieldNames(entityConfig);
    const selectClause = buildSelectClause(fieldNames);
    const whereClause = teamId ? `WHERE id = $1 AND "teamId" = $2` : `WHERE id = $1`;
    const params = teamId ? [id, teamId] : [id];
    const entityToDelete = await queryOneWithRLS(
      `SELECT ${selectClause} FROM ${tableName} ${whereClause}`,
      params,
      userId
    );
    if (!entityToDelete) {
      return false;
    }
    const beforeContext = createHookContext(entitySlug, "delete", userId, void 0, entityToDelete);
    const beforeResult = await executeBeforeHooks(entitySlug, "delete", beforeContext);
    if (!beforeResult.continue) {
      throw new Error(beforeResult.error || "Operation cancelled by hook");
    }
    const result = await mutateWithRLS(
      `DELETE FROM ${tableName} ${whereClause}`,
      params,
      userId
    );
    const deleted = result.rowCount > 0;
    if (deleted) {
      const afterContext = createHookContext(entitySlug, "delete", userId, void 0, entityToDelete);
      executeAfterHooks(entitySlug, "delete", afterContext).catch((err) => {
        console.error(`[GenericEntityService] afterDelete hook error for ${entitySlug}:`, err);
      });
    }
    return deleted;
  }
  /**
   * Check if an entity exists
   *
   * @param entitySlug - The entity type slug
   * @param id - Entity ID
   * @param userId - Current user ID for RLS
   * @param teamId - Team ID for team isolation (prevents cross-team access)
   * @returns true if exists and accessible
   */
  static async exists(entitySlug, id, userId, teamId) {
    if (!(id == null ? void 0 : id.trim()) || !(userId == null ? void 0 : userId.trim())) {
      return false;
    }
    const entityConfig = entityRegistry.get(entitySlug);
    if (!entityConfig) {
      return false;
    }
    const tableName = getTableName(entityConfig);
    const whereClause = teamId ? `WHERE id = $1 AND "teamId" = $2` : `WHERE id = $1`;
    const params = teamId ? [id, teamId] : [id];
    const result = await queryOneWithRLS(
      `SELECT EXISTS(SELECT 1 FROM ${tableName} ${whereClause}) as exists`,
      params,
      userId
    );
    return (result == null ? void 0 : result.exists) ?? false;
  }
  /**
   * Count entities with optional filtering
   *
   * @param entitySlug - The entity type slug
   * @param userId - Current user ID for RLS
   * @param where - Filter conditions
   * @returns Count of matching entities
   */
  static async count(entitySlug, userId, where = {}) {
    var _a;
    if (!(userId == null ? void 0 : userId.trim())) {
      throw new Error("User ID is required");
    }
    const entityConfig = entityRegistry.get(entitySlug);
    if (!entityConfig) {
      throw new Error(`Entity "${entitySlug}" not found in registry`);
    }
    const tableName = getTableName(entityConfig);
    const fieldNames = getFieldNames(entityConfig);
    const allFields = [...SYSTEM_FIELDS, ...fieldNames];
    const { clause, params } = buildWhereClause(where, allFields);
    const result = await queryWithRLS(
      `SELECT COUNT(*)::text as count FROM ${tableName} ${clause}`,
      params,
      userId
    );
    return parseInt(((_a = result[0]) == null ? void 0 : _a.count) || "0", 10);
  }
  /**
   * Delete multiple entities in a single query (batch operation)
   *
   * More efficient than calling delete() in a loop.
   *
   * @param entitySlug - The entity type slug
   * @param ids - Array of entity IDs to delete
   * @param userId - Current user ID for RLS
   * @param options - Optional configuration
   * @param options.executeHooks - If true, executes hooks for each entity (less efficient). Default: false
   * @param options.teamId - Team ID for team isolation (prevents cross-team access)
   * @returns Number of entities deleted
   *
   * @note When executeHooks is false (default), uses a single atomic DELETE query.
   * When executeHooks is true, deletes are performed sequentially and are NOT transactional.
   * If one delete fails mid-batch, previous deletes remain committed.
   */
  static async deleteMany(entitySlug, ids, userId, options = {}) {
    if (!(ids == null ? void 0 : ids.length)) {
      throw new Error("At least one ID is required");
    }
    if (!(userId == null ? void 0 : userId.trim())) {
      throw new Error("User ID is required");
    }
    const entityConfig = entityRegistry.get(entitySlug);
    if (!entityConfig) {
      throw new Error(`Entity "${entitySlug}" not found in registry`);
    }
    const tableName = getTableName(entityConfig);
    const { executeHooks, teamId } = options;
    if (executeHooks) {
      let deletedCount = 0;
      for (const id of ids) {
        const deleted = await GenericEntityService.delete(entitySlug, id, userId, teamId);
        if (deleted) deletedCount++;
      }
      return deletedCount;
    }
    const whereClause = teamId ? `WHERE id = ANY($1) AND "teamId" = $2` : `WHERE id = ANY($1)`;
    const params = teamId ? [ids, teamId] : [ids];
    const result = await mutateWithRLS(
      `DELETE FROM ${tableName} ${whereClause}`,
      params,
      userId
    );
    return result.rowCount;
  }
}
export {
  GenericEntityService,
  validateEntityData
};
