import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Reflector } from '@nestjs/core';
import { AuditService } from './audit.service';
import { JwtPayload } from '../../auth/interfaces/jwt-payload.interface';

export const AUDIT_ACTION_KEY = 'audit_action';
export const AUDIT_ENTITY_KEY = 'audit_entity';

/**
 * Decorator to mark a route for audit logging
 */
export function AuditLog(entity: string, action: string) {
  return (target: object, key?: string | symbol, descriptor?: PropertyDescriptor) => {
    if (descriptor) {
      Reflect.defineMetadata(AUDIT_ACTION_KEY, action, descriptor.value);
      Reflect.defineMetadata(AUDIT_ENTITY_KEY, entity, descriptor.value);
    }
    return descriptor;
  };
}

@Injectable()
export class AuditInterceptor implements NestInterceptor {
  constructor(
    private readonly auditService: AuditService,
    private readonly reflector: Reflector,
  ) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
    const action = this.reflector.get<string>(
      AUDIT_ACTION_KEY,
      context.getHandler(),
    );
    const entity = this.reflector.get<string>(
      AUDIT_ENTITY_KEY,
      context.getHandler(),
    );

    // If no audit metadata, skip logging
    if (!action || !entity) {
      return next.handle();
    }

    const request = context.switchToHttp().getRequest();
    const user = request.user as JwtPayload | undefined;

    // Skip if no authenticated user
    if (!user?.sub) {
      return next.handle();
    }

    const entityId = request.params?.id || undefined;
    const oldData = request.body ? undefined : undefined; // Old data would need to be fetched before operation
    const ipAddress = this.getClientIp(request);
    const userAgent = request.headers['user-agent'];

    return next.handle().pipe(
      tap({
        next: (result) => {
          // Log successful operation
          this.auditService
            .log({
              userId: user.sub,
              action,
              entity,
              entityId,
              oldData,
              newData: this.sanitizeData(result),
              ipAddress,
              userAgent,
            })
            .catch((error) => {
              // Log error but don't fail the request
              console.error('Failed to write audit log:', error);
            });
        },
        error: () => {
          // Optionally log failed operations
          this.auditService
            .log({
              userId: user.sub,
              action: `${action}_FAILED`,
              entity,
              entityId,
              ipAddress,
              userAgent,
            })
            .catch((error) => {
              console.error('Failed to write audit log:', error);
            });
        },
      }),
    );
  }

  /**
   * Extract client IP address from request
   */
  private getClientIp(request: {
    headers: Record<string, string | string[] | undefined>;
    ip?: string;
    connection?: { remoteAddress?: string };
  }): string | undefined {
    const forwardedFor = request.headers['x-forwarded-for'];
    if (forwardedFor) {
      const ips = Array.isArray(forwardedFor)
        ? forwardedFor[0]
        : forwardedFor.split(',')[0];
      return ips.trim();
    }
    return request.ip || request.connection?.remoteAddress;
  }

  /**
   * Remove sensitive data from audit log
   */
  private sanitizeData(
    data: unknown,
  ): Record<string, unknown> | undefined {
    if (!data || typeof data !== 'object') {
      return undefined;
    }

    const sensitiveKeys = ['password', 'token', 'secret', 'credentials', 'bankAccount'];
    const result: Record<string, unknown> = {};

    for (const [key, value] of Object.entries(data as Record<string, unknown>)) {
      if (sensitiveKeys.some((sk) => key.toLowerCase().includes(sk))) {
        result[key] = '[REDACTED]';
      } else if (value && typeof value === 'object' && !Array.isArray(value)) {
        result[key] = this.sanitizeData(value);
      } else {
        result[key] = value;
      }
    }

    return result;
  }
}
