import { query, queryOne, getClient, PoolClient } from '../../config/database.js';
import { NotFoundError, ValidationError } from '../errors/index.js';
import { PaginationMeta } from '../types/index.js';

/**
 * Resultado paginado genérico
 */
export interface PaginatedResult<T> {
  data: T[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
}

/**
 * Filtros de paginación base
 */
export interface BasePaginationFilters {
  page?: number;
  limit?: number;
  sortBy?: string;
  sortOrder?: 'asc' | 'desc';
  search?: string;
}

/**
 * Opciones para construcción de queries
 */
export interface QueryOptions {
  client?: PoolClient;
  includeDeleted?: boolean;
}

/**
 * Configuración del servicio base
 */
export interface BaseServiceConfig {
  tableName: string;
  schema: string;
  selectFields: string;
  searchFields?: string[];
  defaultSortField?: string;
  softDelete?: boolean;
}

/**
 * Clase base abstracta para servicios CRUD con soporte multi-tenant
 *
 * Proporciona implementaciones reutilizables para:
 * - Paginación con filtros
 * - Búsqueda por texto
 * - CRUD básico
 * - Soft delete
 * - Transacciones
 *
 * @example
 * ```typescript
 * class PartnersService extends BaseService<Partner, CreatePartnerDto, UpdatePartnerDto> {
 *   protected config: BaseServiceConfig = {
 *     tableName: 'partners',
 *     schema: 'core',
 *     selectFields: 'id, tenant_id, name, email, phone, created_at',
 *     searchFields: ['name', 'email', 'tax_id'],
 *     defaultSortField: 'name',
 *     softDelete: true,
 *   };
 * }
 * ```
 */
export abstract class BaseService<T, CreateDto, UpdateDto> {
  protected abstract config: BaseServiceConfig;

  /**
   * Nombre completo de la tabla (schema.table)
   */
  protected get fullTableName(): string {
    return `${this.config.schema}.${this.config.tableName}`;
  }

  /**
   * Obtiene todos los registros con paginación y filtros
   */
  async findAll(
    tenantId: string,
    filters: BasePaginationFilters & Record<string, any> = {},
    options: QueryOptions = {}
  ): Promise<PaginatedResult<T>> {
    const {
      page = 1,
      limit = 20,
      sortBy = this.config.defaultSortField || 'created_at',
      sortOrder = 'desc',
      search,
      ...customFilters
    } = filters;

    const offset = (page - 1) * limit;
    const params: any[] = [tenantId];
    let paramIndex = 2;

    // Construir WHERE clause
    let whereClause = 'WHERE tenant_id = $1';

    // Soft delete
    if (this.config.softDelete && !options.includeDeleted) {
      whereClause += ' AND deleted_at IS NULL';
    }

    // Búsqueda por texto
    if (search && this.config.searchFields?.length) {
      const searchConditions = this.config.searchFields
        .map(field => `${field} ILIKE $${paramIndex}`)
        .join(' OR ');
      whereClause += ` AND (${searchConditions})`;
      params.push(`%${search}%`);
      paramIndex++;
    }

    // Filtros custom
    for (const [key, value] of Object.entries(customFilters)) {
      if (value !== undefined && value !== null && value !== '') {
        whereClause += ` AND ${key} = $${paramIndex++}`;
        params.push(value);
      }
    }

    // Validar sortBy para prevenir SQL injection
    const safeSortBy = this.sanitizeFieldName(sortBy);
    const safeSortOrder = sortOrder === 'asc' ? 'ASC' : 'DESC';

    // Query de conteo
    const countSql = `
      SELECT COUNT(*) as count
      FROM ${this.fullTableName}
      ${whereClause}
    `;

    // Query de datos
    const dataSql = `
      SELECT ${this.config.selectFields}
      FROM ${this.fullTableName}
      ${whereClause}
      ORDER BY ${safeSortBy} ${safeSortOrder}
      LIMIT $${paramIndex++} OFFSET $${paramIndex}
    `;

    if (options.client) {
      const [countResult, dataResult] = await Promise.all([
        options.client.query(countSql, params),
        options.client.query(dataSql, [...params, limit, offset]),
      ]);

      const total = parseInt(countResult.rows[0]?.count || '0', 10);

      return {
        data: dataResult.rows as T[],
        total,
        page,
        limit,
        totalPages: Math.ceil(total / limit),
      };
    }

    const [countRows, dataRows] = await Promise.all([
      query<{ count: string }>(countSql, params),
      query<T>(dataSql, [...params, limit, offset]),
    ]);

    const total = parseInt(countRows[0]?.count || '0', 10);

    return {
      data: dataRows,
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
    };
  }

  /**
   * Obtiene un registro por ID
   */
  async findById(
    id: string,
    tenantId: string,
    options: QueryOptions = {}
  ): Promise<T | null> {
    let whereClause = 'WHERE id = $1 AND tenant_id = $2';

    if (this.config.softDelete && !options.includeDeleted) {
      whereClause += ' AND deleted_at IS NULL';
    }

    const sql = `
      SELECT ${this.config.selectFields}
      FROM ${this.fullTableName}
      ${whereClause}
    `;

    if (options.client) {
      const result = await options.client.query(sql, [id, tenantId]);
      return result.rows[0] as T || null;
    }
    const rows = await query<T>(sql, [id, tenantId]);
    return rows[0] || null;
  }

  /**
   * Obtiene un registro por ID o lanza error si no existe
   */
  async findByIdOrFail(
    id: string,
    tenantId: string,
    options: QueryOptions = {}
  ): Promise<T> {
    const entity = await this.findById(id, tenantId, options);
    if (!entity) {
      throw new NotFoundError(`${this.config.tableName} with id ${id} not found`);
    }
    return entity;
  }

  /**
   * Verifica si existe un registro
   */
  async exists(
    id: string,
    tenantId: string,
    options: QueryOptions = {}
  ): Promise<boolean> {
    let whereClause = 'WHERE id = $1 AND tenant_id = $2';

    if (this.config.softDelete && !options.includeDeleted) {
      whereClause += ' AND deleted_at IS NULL';
    }

    const sql = `
      SELECT 1 FROM ${this.fullTableName}
      ${whereClause}
      LIMIT 1
    `;

    if (options.client) {
      const result = await options.client.query(sql, [id, tenantId]);
      return result.rows.length > 0;
    }
    const rows = await query(sql, [id, tenantId]);
    return rows.length > 0;
  }

  /**
   * Soft delete de un registro
   */
  async softDelete(
    id: string,
    tenantId: string,
    userId: string,
    options: QueryOptions = {}
  ): Promise<boolean> {
    if (!this.config.softDelete) {
      throw new ValidationError('Soft delete not enabled for this entity');
    }

    const sql = `
      UPDATE ${this.fullTableName}
      SET deleted_at = CURRENT_TIMESTAMP,
          deleted_by = $3,
          updated_at = CURRENT_TIMESTAMP
      WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
      RETURNING id
    `;

    if (options.client) {
      const result = await options.client.query(sql, [id, tenantId, userId]);
      return result.rows.length > 0;
    }
    const rows = await query(sql, [id, tenantId, userId]);
    return rows.length > 0;
  }

  /**
   * Hard delete de un registro
   */
  async hardDelete(
    id: string,
    tenantId: string,
    options: QueryOptions = {}
  ): Promise<boolean> {
    const sql = `
      DELETE FROM ${this.fullTableName}
      WHERE id = $1 AND tenant_id = $2
      RETURNING id
    `;

    if (options.client) {
      const result = await options.client.query(sql, [id, tenantId]);
      return result.rows.length > 0;
    }
    const rows = await query(sql, [id, tenantId]);
    return rows.length > 0;
  }

  /**
   * Cuenta registros con filtros
   */
  async count(
    tenantId: string,
    filters: Record<string, any> = {},
    options: QueryOptions = {}
  ): Promise<number> {
    const params: any[] = [tenantId];
    let paramIndex = 2;
    let whereClause = 'WHERE tenant_id = $1';

    if (this.config.softDelete && !options.includeDeleted) {
      whereClause += ' AND deleted_at IS NULL';
    }

    for (const [key, value] of Object.entries(filters)) {
      if (value !== undefined && value !== null) {
        whereClause += ` AND ${key} = $${paramIndex++}`;
        params.push(value);
      }
    }

    const sql = `
      SELECT COUNT(*) as count
      FROM ${this.fullTableName}
      ${whereClause}
    `;

    if (options.client) {
      const result = await options.client.query(sql, params);
      return parseInt(result.rows[0]?.count || '0', 10);
    }
    const rows = await query<{ count: string }>(sql, params);
    return parseInt(rows[0]?.count || '0', 10);
  }

  /**
   * Ejecuta una función dentro de una transacción
   */
  protected async withTransaction<R>(
    fn: (client: PoolClient) => Promise<R>
  ): Promise<R> {
    const client = await getClient();
    try {
      await client.query('BEGIN');
      const result = await fn(client);
      await client.query('COMMIT');
      return result;
    } catch (error) {
      await client.query('ROLLBACK');
      throw error;
    } finally {
      client.release();
    }
  }

  /**
   * Sanitiza nombre de campo para prevenir SQL injection
   */
  protected sanitizeFieldName(field: string): string {
    // Solo permite caracteres alfanuméricos y guiones bajos
    if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(field)) {
      return this.config.defaultSortField || 'created_at';
    }
    return field;
  }

  /**
   * Construye un INSERT dinámico
   */
  protected buildInsertQuery(
    data: Record<string, any>,
    additionalFields: Record<string, any> = {}
  ): { sql: string; params: any[] } {
    const allData = { ...data, ...additionalFields };
    const fields = Object.keys(allData);
    const values = Object.values(allData);
    const placeholders = fields.map((_, i) => `$${i + 1}`);

    const sql = `
      INSERT INTO ${this.fullTableName} (${fields.join(', ')})
      VALUES (${placeholders.join(', ')})
      RETURNING ${this.config.selectFields}
    `;

    return { sql, params: values };
  }

  /**
   * Construye un UPDATE dinámico
   */
  protected buildUpdateQuery(
    id: string,
    tenantId: string,
    data: Record<string, any>
  ): { sql: string; params: any[] } {
    const fields = Object.keys(data).filter(k => data[k] !== undefined);
    const setClauses = fields.map((f, i) => `${f} = $${i + 1}`);
    const values = fields.map(f => data[f]);

    // Agregar updated_at automáticamente
    setClauses.push(`updated_at = CURRENT_TIMESTAMP`);

    const paramIndex = fields.length + 1;

    const sql = `
      UPDATE ${this.fullTableName}
      SET ${setClauses.join(', ')}
      WHERE id = $${paramIndex} AND tenant_id = $${paramIndex + 1}
      RETURNING ${this.config.selectFields}
    `;

    return { sql, params: [...values, id, tenantId] };
  }

  /**
   * Redondea a N decimales
   */
  protected roundToDecimals(value: number, decimals: number = 2): number {
    const factor = Math.pow(10, decimals);
    return Math.round(value * factor) / factor;
  }
}

export default BaseService;
