import {
  Repository,
  FindManyOptions,
  FindOneOptions,
  DeepPartial,
  FindOptionsWhere,
  SaveOptions,
  ILike,
} from 'typeorm';
import { ClsService } from 'nestjs-cls';
import { BaseEntity } from '../../database/base.entity';
import { CLS_TENANT_ID, CLS_USER_ID } from '../cls/cls.constants';

/**
 * TenantAwareRepository — auto-scopes ALL queries by tenant_id via CLS.
 *
 * Per CLAUDE.md:
 * - TenantAwareRepository reads tenant_id from ClsService automatically
 * - No setTenantId() calls — context propagation is fully automatic
 * - created_by and updated_by also read from ClsService automatically
 * - Soft delete sets is_deleted = true (never hard delete)
 *
 * ⚠️ Raw SQL must manually add tenant_id (CLS not available in raw queries)
 * ⚠️ Cross-tenant queries not allowed in Phase 1
 *
 * Usage in feature modules:
 *   @Injectable()
 *   export class HotelRepository extends TenantAwareRepository<HotelEntity> {
 *     constructor(
 *       @InjectRepository(HotelEntity) repo: Repository<HotelEntity>,
 *       cls: ClsService,
 *     ) {
 *       super(repo, cls);
 *     }
 *   }
 */
export abstract class TenantAwareRepository<T extends BaseEntity> {
  constructor(
    protected readonly repo: Repository<T>,
    protected readonly cls: ClsService,
  ) {}

  /**
   * Get tenant_id from CLS — fails fast if missing.
   */
  protected getTenantId(): string {
    const tenantId = this.cls.get<string>(CLS_TENANT_ID);
    if (!tenantId) {
      throw new Error(
        'TenantAwareRepository: tenant_id not found in CLS context. ' +
        'Ensure TenantMiddleware has run for this request.',
      );
    }
    return tenantId;
  }

  /**
   * Get current user_id from CLS (for audit trail).
   * Returns null if user is not authenticated (e.g. public endpoints).
   */
  protected getUserId(): string | null {
    return this.cls.get<string>(CLS_USER_ID) || null;
  }

  /**
   * Scoped find — auto-adds tenant_id and is_deleted filters.
   */
  async find(options?: FindManyOptions<T>): Promise<T[]> {
    const where = {
      ...(options?.where || {}),
      tenant_id: this.getTenantId(),
      is_deleted: false,
    } as FindOptionsWhere<T>;

    return this.repo.find({ ...options, where });
  }

  /**
   * Scoped findOne — auto-adds tenant_id and is_deleted filters.
   */
  async findOne(options: FindOneOptions<T>): Promise<T | null> {
    const where = {
      ...(options.where || {}),
      tenant_id: this.getTenantId(),
      is_deleted: false,
    } as FindOptionsWhere<T>;

    return this.repo.findOne({ ...options, where });
  }

  /**
   * Scoped findById — convenience wrapper.
   */
  async findById(id: string): Promise<T | null> {
    return this.findOne({
      where: { id } as FindOptionsWhere<T>,
    });
  }

  /**
   * Scoped save — auto-injects tenant_id, created_by, updated_by.
   */
  async save(entity: DeepPartial<T>, options?: SaveOptions): Promise<T> {
    const userId = this.getUserId();
    const entityWithContext = {
      ...entity,
      tenant_id: this.getTenantId(),
      created_by: (entity as any).created_by !== undefined
        ? (entity as any).created_by
        : userId,
      updated_by: userId,
    } as DeepPartial<T>;

    return this.repo.save(entityWithContext, options);
  }

  /**
   * Scoped saveMany — auto-injects tenant_id, created_by, updated_by.
   */
  async saveMany(entities: DeepPartial<T>[], options?: SaveOptions): Promise<T[]> {
    const userId = this.getUserId();
    const tenantId = this.getTenantId();

    const entitiesWithContext = entities.map((entity) => ({
      ...entity,
      tenant_id: tenantId,
      created_by: (entity as any).created_by !== undefined
        ? (entity as any).created_by
        : userId,
      updated_by: userId,
    })) as DeepPartial<T>[];

    return this.repo.save(entitiesWithContext, options);
  }

  /**
   * Scoped update — updates entity fields within tenant scope.
   * Auto-sets updated_by from CLS.
   */
  async update(id: string, data: Partial<T>): Promise<void> {
    const userId = this.getUserId();
    await this.repo.update(
      {
        id,
        tenant_id: this.getTenantId(),
      } as any,
      {
        ...data,
        updated_by: userId,
      } as any,
    );
  }

  /**
   * Soft delete — sets is_deleted = true. Never hard deletes.
   * Auto-sets updated_by from CLS.
   */
  async softDelete(id: string): Promise<void> {
    const userId = this.getUserId();
    await this.repo.update(
      {
        id,
        tenant_id: this.getTenantId(),
      } as any,
      {
        is_deleted: true,
        updated_by: userId,
      } as any,
    );
  }

  /**
   * Scoped count — auto-adds tenant_id and is_deleted filters.
   */
  async count(options?: FindManyOptions<T>): Promise<number> {
    const where = {
      ...(options?.where || {}),
      tenant_id: this.getTenantId(),
      is_deleted: false,
    } as FindOptionsWhere<T>;

    return this.repo.count({ ...options, where });
  }

  /**
   * Paginated find — returns items + pagination meta.
   * Supports search across specified columns via ILike.
   */
  async findPaginated(options: {
    page?: number;
    limit?: number;
    search?: string;
    searchColumns?: string[];
    relations?: string[];
    order?: Record<string, 'ASC' | 'DESC'>;
    where?: FindOptionsWhere<T>;
  }): Promise<{ items: T[]; meta: { total: number; page: number; limit: number; totalPages: number } }> {

    const page = options.page || 1;
    const limit = options.limit || 10;
    const skip = (page - 1) * limit;
    const tenantId = this.getTenantId();

    const baseWhere = {
      ...(options.where || {}),
      tenant_id: tenantId,
      is_deleted: false,
    } as any;

    // Build search conditions
    // Single word: OR across columns. Multi-word: concat all columns, match each word (AND).
    let whereConditions: any;
    const trimmedSearch = options.search?.trim();
    if (trimmedSearch && options.searchColumns?.length) {
      // Strip arrows and special chars (keep hyphens — used in IDs like BK-002)
      const cleaned = trimmedSearch.replace(/[→←↔>/\\|]+/g, ' ');
      const words = cleaned.split(/\s+/).filter(w => w.length > 0);
      if (words.length === 0) {
        // All chars were special (e.g. just "→") — no meaningful search
        whereConditions = baseWhere;
      } else if (words.length === 1) {
        // Single word: OR across all columns (use cleaned word, not raw input)
        whereConditions = options.searchColumns.map((col) => ({
          ...baseWhere,
          [col]: ILike(`%${words[0]}%`),
        }));
      } else {
        // Multi-word: fall back to matching each word against any column (OR per word)
        // This returns results where ALL words appear in at least one searchable column
        // Simple approach: for each word, create OR conditions across columns, then intersect
        // TypeORM where array = OR, so we can't do AND across words with simple find.
        // Match original search string first (exact phrase like "BK-002")
        // PLUS cleaned version for cross-column cases (catches "Toyota Hiace")
        const originalMatchConditions = options.searchColumns.map((col) => ({
          ...baseWhere,
          [col]: ILike(`%${trimmedSearch}%`),
        }));
        const cleanedSearch = cleaned.replace(/\s+/g, ' ').trim();
        const fullMatchConditions = cleanedSearch !== trimmedSearch
          ? options.searchColumns.map((col) => ({
              ...baseWhere,
              [col]: ILike(`%${cleanedSearch}%`),
            }))
          : [];
        // Also add per-word conditions: each word matched in any column
        const perWordConditions = words.flatMap((word) =>
          options.searchColumns!.map((col) => ({
            ...baseWhere,
            [col]: ILike(`%${word}%`),
          }))
        );
        whereConditions = [...originalMatchConditions, ...fullMatchConditions, ...perWordConditions];
      }
    } else {
      whereConditions = baseWhere;
    }

    const [items, total] = await this.repo.findAndCount({
      where: whereConditions,
      relations: options.relations,
      order: options.order as any,
      skip,
      take: limit,
    });

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