import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Between, FindOptionsWhere } from 'typeorm';
import { ClsService } from 'nestjs-cls';
import { AuditLogEntity } from '../../../entities/audit-log.entity';
import {
  CLS_TENANT_ID,
  CLS_USER_ID,
  CLS_IP_ADDRESS,
  CLS_USER_AGENT,
  CLS_REQUEST_ID,
} from '../../../common/cls/cls.constants';
import { AuditChange } from '../audit-types';

export type AuditFilters = {
  page?: number;
  limit?: number;
  entity_type?: string;
  action?: string;
  performed_by?: string;
  from?: string;
  to?: string;
};

/**
 * Extra fields we attach onto rows whose entity_type is one of the
 * booking item tables (transport / activity / hotel / special).
 * Lets the FE build a deep link to Movement Chart on the right date
 * with the right op-id to scroll to.
 */
export interface AuditItemContext {
  booking_id: string | null;
  item_date: string | null;
}

const ITEM_ENTITY_TABLES: Record<string, { table: string; dateCol: string }> = {
  booking_transport_item: { table: 'booking_transport_items', dateCol: 'date' },
  booking_activity_item: { table: 'booking_activity_items', dateCol: 'date' },
  booking_hotel_item: { table: 'booking_hotel_items', dateCol: 'check_in_date' },
  booking_special_item: { table: 'booking_special_items', dateCol: 'date' },
};

@Injectable()
export class AuditRepository {
  constructor(
    @InjectRepository(AuditLogEntity)
    private readonly repo: Repository<AuditLogEntity>,
    private readonly cls: ClsService,
  ) {}

  private getTenantId(): string {
    const tenantId = this.cls.get<string>(CLS_TENANT_ID);
    if (!tenantId) {
      throw new Error('AuditRepository: tenant_id not found in CLS context.');
    }
    return tenantId;
  }

  private getUserId(): string | null {
    return this.cls.get<string>(CLS_USER_ID) || null;
  }

  async log(
    entityType: string,
    entityId: string,
    action: string,
    changes?: AuditChange[] | null,
  ): Promise<AuditLogEntity> {
    return this.repo.save({
      tenant_id: this.getTenantId(),
      entity_type: entityType,
      entity_id: entityId,
      action,
      changes: changes && changes.length > 0 ? changes : null,
      performed_by: this.getUserId(),
      ip_address: this.cls.get<string>(CLS_IP_ADDRESS) || null,
      user_agent: this.cls.get<string>(CLS_USER_AGENT) || null,
      request_id: this.cls.get<string>(CLS_REQUEST_ID) || null,
    });
  }

  async findRecent(limit = 10): Promise<AuditLogEntity[]> {
    return this.repo.find({
      where: { tenant_id: this.getTenantId() },
      relations: {
        performer: true,
      },
      order: { created_at: 'DESC' },
      take: limit,
    });
  }

  async findByEntity(
    entityType: string,
    entityId: string,
    options?: { page?: number; limit?: number },
  ): Promise<{
    items: AuditLogEntity[];
    meta: { total: number; page: number; limit: number; totalPages: number };
  }> {
    const page = options?.page || 1;
    const limit = options?.limit || 50;
    const skip = (page - 1) * limit;

    const [items, total] = await this.repo.findAndCount({
      where: {
        tenant_id: this.getTenantId(),
        entity_type: entityType,
        entity_id: entityId,
      },
      relations: {
        performer: true,
      },
      order: { created_at: 'DESC' },
      skip,
      take: limit,
    });

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

  /**
   * Enrich item-typed audit rows with their parent booking_id + item
   * date. Returns a map keyed by the item's own id so callers can
   * attach the context to the audit row without changing its type.
   * Rows for other entity types are simply absent from the map.
   */
  async loadItemContext(items: AuditLogEntity[]): Promise<Map<string, AuditItemContext>> {
    const byType = new Map<string, string[]>();
    for (const it of items) {
      const cfg = ITEM_ENTITY_TABLES[it.entity_type];
      if (!cfg) continue;
      const bucket = byType.get(it.entity_type) || [];
      bucket.push(it.entity_id);
      byType.set(it.entity_type, bucket);
    }

    const out = new Map<string, AuditItemContext>();
    if (byType.size === 0) return out;

    const tenantId = this.getTenantId();
    for (const [entityType, ids] of byType) {
      const cfg = ITEM_ENTITY_TABLES[entityType];
      // Cast date to text so pg keeps it as YYYY-MM-DD instead of
      // parsing it into a JS Date (which then JSON-serializes as
      // an ISO datetime and shifts the day across timezones).
      const rows: Array<{ id: string; booking_id: string | null; item_date: string | null }> =
        await this.repo.manager.query(
          `SELECT id, booking_id, ${cfg.dateCol}::text AS item_date
             FROM ${cfg.table}
            WHERE tenant_id = $1
              AND id = ANY($2::uuid[])`,
          [tenantId, [...new Set(ids)]],
        );
      for (const r of rows) {
        out.set(r.id, { booking_id: r.booking_id, item_date: r.item_date });
      }
    }
    return out;
  }

  async findFiltered(filters: AuditFilters): Promise<{
    items: AuditLogEntity[];
    meta: { total: number; page: number; limit: number; totalPages: number };
  }> {
    const page = filters.page || 1;
    const limit = Math.min(filters.limit || 50, 100);
    const skip = (page - 1) * limit;

    const where: FindOptionsWhere<AuditLogEntity> = { tenant_id: this.getTenantId() };
    if (filters.entity_type) where.entity_type = filters.entity_type;
    if (filters.action) where.action = filters.action;
    if (filters.performed_by) where.performed_by = filters.performed_by;
    if (filters.from || filters.to) {
      const from = filters.from ? new Date(filters.from) : new Date('1970-01-01');
      const to = filters.to ? new Date(filters.to + 'T23:59:59') : new Date();
      where.created_at = Between(from, to);
    }

    const [items, total] = await this.repo.findAndCount({
      where,
      relations: {
        performer: true,
      },
      order: { created_at: 'DESC' },
      skip,
      take: limit,
    });

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