import { ApiException } from "../../exceptions";
import { ListEndpoint } from "../list";
import type { ListFilters, Logger } from "../types";
import {
  buildOrderByClause,
  buildWhereClause,
  getD1Binding,
  validateColumnName,
  validateOrderByColumn,
  validateOrderDirection,
  validateTableName,
} from "./base";

/**
 * D1-specific ListEndpoint implementation.
 * Provides automatic SELECT with pagination, filtering, and ordering.
 * Includes SQL injection prevention for all query components.
 */
export class D1ListEndpoint<HandleArgs extends Array<object> = Array<object>> extends ListEndpoint<HandleArgs> {
  /** Name of the D1 database binding in the worker environment. Defaults to "DB" */
  dbName = "DB";
  /** Optional logger for debugging and error tracking */
  logger?: Logger;
  /** Maximum number of results per page. Override to change the limit. */
  maxPerPage = 100;

  /**
   * Gets the D1 database binding from the worker environment.
   * @returns D1Database instance
   * @throws ApiException if binding is not defined or is not a D1 binding
   */
  getDBBinding(): D1Database {
    return getD1Binding((args) => this.params.router.getBindings(args), this.args, this.dbName);
  }

  /**
   * Gets the list of valid column names from the model schema.
   * @returns Array of valid column names
   */
  protected getValidColumns(): string[] {
    return Object.keys(this.meta.model.schema.shape);
  }

  /**
   * Lists records with pagination, filtering, and ordering.
   * @param filters - Filter conditions and pagination options
   * @returns Object containing results and pagination info
   */
  async list(filters: {
    filters?: Array<{ field: string; operator: string; value: unknown }>;
    options?: ListFilters["options"];
  }) {
    const tableName = validateTableName(this.meta.model.tableName);
    const validColumns = this.getValidColumns();

    // Safe pagination defaults — use custom field names for option keys
    const perPage = Math.min(Math.max(1, Number(filters?.options?.[this.perPageFieldName]) || 20), this.maxPerPage);
    const page = Math.max(1, Number(filters?.options?.[this.pageFieldName]) || 1);
    const limit = perPage;
    const offset = Math.max(0, (page - 1) * perPage);

    // Build filter conditions
    const conditions: string[] = [];
    const conditionsParams: unknown[] = [];

    for (const f of filters?.filters || []) {
      // Handle search field specially
      if (this.searchFields && this.searchFields.length > 0 && f.field === this.searchFieldName) {
        // Validate all search fields
        const validatedSearchFields = this.searchFields.map((col) => validateColumnName(col, validColumns));

        const searchCondition = validatedSearchFields
          .map((col) => `UPPER(${col}) LIKE UPPER(?${conditionsParams.length + 1}) ESCAPE '\\'`)
          .join(" OR ");

        conditions.push(`(${searchCondition})`);
        // Escape the ESCAPE character itself first, then LIKE wildcards
        const escaped = String(f.value).replace(/\\/g, "\\\\").replace(/[%_]/g, "\\$&");
        conditionsParams.push(`%${escaped}%`);
      } else if (f.operator === "EQ") {
        // Validate filter column
        const validatedColumn = validateColumnName(f.field, validColumns);
        conditions.push(`${validatedColumn} = ?${conditionsParams.length + 1}`);
        conditionsParams.push(f.value);
      } else {
        throw new ApiException(`Operator "${f.operator}" is not implemented`);
      }
    }

    const whereClause = buildWhereClause(conditions);

    // Build safe ORDER BY clause
    const orderByFields = Array.isArray(this.orderByFields) ? this.orderByFields : [];
    const primaryKey = this.meta?.model?.primaryKeys?.[0] || "id";

    // Determine the fallback column (defaultOrderBy or first primary key)
    const fallbackColumn =
      typeof this.defaultOrderBy === "string" && this.defaultOrderBy !== "undefined" ? this.defaultOrderBy : primaryKey;

    // Get validated order column — use custom field names for option keys
    const orderColumn = validateOrderByColumn(
      filters?.options?.[this.orderByFieldName] as string | undefined,
      orderByFields,
      fallbackColumn,
    );

    const orderDirection = validateOrderDirection(
      filters?.options?.[this.orderByDirectionFieldName] as string | undefined,
    );
    const orderByClause = buildOrderByClause(orderColumn, orderDirection);

    // Build final SQL
    const dataSql = `SELECT * FROM ${tableName} ${whereClause} ${orderByClause} LIMIT ${limit} OFFSET ${offset}`;
    const countSql = `SELECT COUNT(*) as total FROM ${tableName} ${whereClause}`;

    if (this.logger) {
      this.logger.debug?.(`[D1ListEndpoint] SQL: ${dataSql}`, conditionsParams);
    }

    // Execute queries
    const [results, totalCount] = await Promise.all([
      this.getDBBinding()
        .prepare(dataSql)
        .bind(...conditionsParams)
        .all(),
      this.getDBBinding()
        .prepare(countSql)
        .bind(...conditionsParams)
        .all(),
    ]);

    return {
      result: results.results,
      result_info: {
        count: results.results.length,
        page,
        per_page: perPage,
        total_count: (totalCount.results[0] as { total: number })?.total ?? 0,
      },
    };
  }
}
