import {
  type Repository,
  type EntityManager,
  type FindManyOptions,
  type FindOneOptions,
  type FindOptionsOrder,
  type DeepPartial,
  type FindOptionsWhere,
  type SaveOptions,
  ILike,
} from 'typeorm';
import { type ClsService } from 'nestjs-cls';
import { type 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;
  }

  // ────────────────────────────────────────────────────────────────────
  // Public accessors for cross-service backfills + raw-SQL escape hatches
  //
  // Services that legitimately need to drive raw SQL with the current
  // tenant context (e.g. bulk-insert pricing rows across services) used
  // to bypass `protected` via `(this.repo as any)` casts. These typed
  // accessors give the same capability without the escape hatch.
  //
  // ⚠️ Callers using `manager` are still responsible for manually
  // adding `tenant_id` to their WHERE / INSERT clauses — raw SQL
  // bypasses the auto-scoping of the public scoped methods.
  // ────────────────────────────────────────────────────────────────────

  /** Underlying ORM EntityManager — for raw SQL / cross-entity work. */
  get manager(): EntityManager {
    return this.repo.manager;
  }

  /** Current tenant_id from CLS — public wrapper around protected `getTenantId()`. */
  currentTenantId(): string {
    return this.getTenantId();
  }

  /** Current user_id from CLS — public wrapper around protected `getUserId()`. */
  currentUserId(): string | null {
    return this.getUserId();
  }

  /**
   * 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 Partial<BaseEntity>).created_by !== undefined
          ? (entity as Partial<BaseEntity>).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 Partial<BaseEntity>).created_by !== undefined
          ? (entity as Partial<BaseEntity>).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 FindOptionsWhere<T>,
      {
        ...data,
        updated_by: userId,
        // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TypeORM QueryDeepPartialEntity inference fails on generic T
      } 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 FindOptionsWhere<T>,
      {
        is_deleted: true,
        updated_by: userId,
        // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TypeORM QueryDeepPartialEntity inference fails on generic T
      } 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?: FindOptionsOrder<T>;
    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 FindOptionsWhere<T>;

    // Build search conditions
    // Single word: OR across columns. Multi-word: concat all columns, match each word (AND).
    let whereConditions: FindOptionsWhere<T> | FindOptionsWhere<T>[];
    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: treat as a literal phrase. `"Half Day"` should find
        // "Half Day Dubai City Tour" but NOT "Sunday Cruise" (which the
        // previous per-word OR fanout would surface just because "Day"
        // matched). Users type multi-word searches when they want a
        // phrase — that's the workflow this branch optimises for.
        //
        // Two conditions OR'd together:
        //   - `originalMatchConditions` — matches the raw input, so hyphens
        //     and slashes ("BK-002", "N/A") survive verbatim
        //   - `cleanedMatchConditions` — collapses arrows/pipes to spaces
        //     ("Dubai → Marina" → "Dubai Marina") so route-style searches
        //     still work when the arrow only lives on one side
        const originalMatchConditions = options.searchColumns.map((col) => ({
          ...baseWhere,
          [col]: ILike(`%${trimmedSearch}%`),
        }));
        const cleanedSearch = cleaned.replace(/\s+/g, ' ').trim();
        const cleanedMatchConditions =
          cleanedSearch !== trimmedSearch
            ? options.searchColumns.map((col) => ({
                ...baseWhere,
                [col]: ILike(`%${cleanedSearch}%`),
              }))
            : [];
        whereConditions = [
          ...originalMatchConditions,
          ...cleanedMatchConditions,
        ];
      }
    } else {
      whereConditions = baseWhere;
    }

    // TypeORM 1.0: `relations` now requires the object form
    // (`{ rel: true, ... }`) instead of a string array. We keep the
    // public API as `string[]` for callers' simplicity and convert
    // internally. Dot-paths (e.g. 'tags.users') become nested objects.
    const relationsObj = options.relations?.reduce<Record<string, unknown>>((acc, path) => {
      const parts = path.split('.');
      let cursor = acc;
      for (let i = 0; i < parts.length; i++) {
        const part = parts[i];
        if (i === parts.length - 1) {
          cursor[part] = true;
        } else {
          if (typeof cursor[part] !== 'object' || cursor[part] === null) {
            cursor[part] = {};
          }
          cursor = cursor[part] as Record<string, unknown>;
        }
      }
      return acc;
    }, {});
    const [items, total] = await this.repo.findAndCount({
      where: whereConditions,
      relations: relationsObj as FindManyOptions<T>['relations'],
      order: options.order,
      skip,
      take: limit,
    });

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