import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Between, FindOptionsWhere, Like, Repository } from 'typeorm';
import { AuditLog } from './audit-log.entity';

export interface AuditEntry {
  userId?: number;
  action: string;
  entityType?: string;
  entityId?: number;
  oldValue?: Record<string, any>;
  newValue?: Record<string, any>;
  ip?: string;
}

export interface AuditQuery {
  userId?: number;
  action?: string;
  entityType?: string;
  entityId?: number;
  from?: string;
  to?: string;
  page?: number;
  limit?: number;
}

/** Keys that must never reach the audit trail. */
const REDACTED_KEYS = [
  'password',
  'passwordhash',
  'password_hash',
  'newpassword',
  'currentpassword',
  'token',
  'accesstoken',
  'access_token',
  'refreshtoken',
  'refresh_token',
  'secret',
  'authorization',
];

@Injectable()
export class AuditLogsService {
  private readonly logger = new Logger(AuditLogsService.name);

  constructor(
    @InjectRepository(AuditLog)
    private readonly auditRepo: Repository<AuditLog>,
  ) {}

  /** Strips secrets out of an arbitrary payload before it is persisted. */
  static sanitize(value: any): any {
    if (value === null || value === undefined) return value;
    if (Array.isArray(value)) return value.map((v) => AuditLogsService.sanitize(v));
    if (typeof value !== 'object') return value;

    const out: Record<string, any> = {};
    for (const [key, val] of Object.entries(value)) {
      if (REDACTED_KEYS.includes(key.toLowerCase())) {
        out[key] = '[REDACTED]';
      } else {
        out[key] = AuditLogsService.sanitize(val);
      }
    }
    return out;
  }

  /**
   * Audit writes must never break the request that triggered them, so
   * failures are logged and swallowed.
   */
  async record(entry: AuditEntry): Promise<void> {
    try {
      await this.auditRepo.save(
        this.auditRepo.create({
          userId: entry.userId,
          action: entry.action,
          entityType: entry.entityType,
          entityId: entry.entityId,
          oldValue: AuditLogsService.sanitize(entry.oldValue),
          newValue: AuditLogsService.sanitize(entry.newValue),
          ip: entry.ip,
        }),
      );
    } catch (err) {
      this.logger.warn(
        `Failed to write audit log for ${entry.action}: ${(err as Error).message}`,
      );
    }
  }

  async findAll(query: AuditQuery) {
    const page = Math.max(1, Number(query.page) || 1);
    const limit = Math.min(200, Math.max(1, Number(query.limit) || 50));

    const where: FindOptionsWhere<AuditLog> = {};
    if (query.userId) where.userId = Number(query.userId);
    if (query.action) where.action = Like(`%${query.action}%`);
    if (query.entityType) where.entityType = query.entityType;
    if (query.entityId) where.entityId = Number(query.entityId);
    if (query.from && query.to) {
      where.createdAt = Between(new Date(query.from), new Date(query.to));
    }

    const [items, total] = await this.auditRepo.findAndCount({
      where,
      relations: ['user'],
      order: { createdAt: 'DESC' },
      skip: (page - 1) * limit,
      take: limit,
    });

    return {
      items: items.map((item) => ({
        ...item,
        user: item.user
          ? { id: item.user.id, email: item.user.email, displayName: item.user.displayName }
          : null,
      })),
      meta: { page, limit, total, totalPages: Math.ceil(total / limit) },
    };
  }

  async findByEntity(entityType: string, entityId: number): Promise<AuditLog[]> {
    return this.auditRepo.find({
      where: { entityType, entityId },
      relations: ['user'],
      order: { createdAt: 'DESC' },
    });
  }

  countSince(since: Date): Promise<number> {
    return this.auditRepo.count({ where: { createdAt: Between(since, new Date()) } });
  }
}
