import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Request } from 'express';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { AuditLogsService } from '../../audit-logs/audit-logs.service';
import { AUDIT_ACTION_KEY, AUDIT_SKIP_KEY } from '../decorators/audit.decorator';

const MUTATING_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE'];

/** Trailing path verbs that describe an action more precisely than the HTTP verb. */
const VERB_ACTIONS: Record<string, string> = {
  publish: 'PUBLISHED',
  unpublish: 'UNPUBLISHED',
  archive: 'ARCHIVED',
  restore: 'RESTORED',
  approve: 'APPROVED',
  reject: 'REJECTED',
  'submit-review': 'SUBMITTED_FOR_REVIEW',
  reorder: 'REORDERED',
  roles: 'ROLES_ASSIGNED',
  permissions: 'PERMISSIONS_ASSIGNED',
  activate: 'ACTIVATED',
  deactivate: 'DEACTIVATED',
};

const METHOD_ACTIONS: Record<string, string> = {
  POST: 'CREATED',
  PUT: 'UPDATED',
  PATCH: 'UPDATED',
  DELETE: 'DELETED',
};

function singularize(plural: string): string {
  const base = plural.replace(/-/g, '_');
  if (base.endsWith('ies')) return `${base.slice(0, -3)}y`;
  if (base.endsWith('ses')) return base.slice(0, -2);
  if (base.endsWith('s') && !base.endsWith('ss')) return base.slice(0, -1);
  return base;
}

/**
 * Writes an audit entry for every mutating request under /api/v1/admin.
 * Routes opt out with @SkipAudit() and rename their action with @AuditAction().
 */
@Injectable()
export class AuditInterceptor implements NestInterceptor {
  constructor(
    private readonly auditService: AuditLogsService,
    private readonly reflector: Reflector,
  ) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    if (context.getType() !== 'http') return next.handle();

    const request = context.switchToHttp().getRequest<Request>();
    const skip = this.reflector.getAllAndOverride<boolean>(AUDIT_SKIP_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (
      skip ||
      !MUTATING_METHODS.includes(request.method) ||
      !request.path.includes('/admin/')
    ) {
      return next.handle();
    }

    const explicitAction = this.reflector.getAllAndOverride<string>(AUDIT_ACTION_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    // Snapshot the body now — downstream code is free to mutate it.
    const requestBody = AuditLogsService.sanitize(request.body);

    return next.handle().pipe(
      tap((response) => {
        // A handler that reported failure did not change anything worth logging.
        if (response && typeof response === 'object' && response.success === false) {
          return;
        }

        const { entityType, entityId, verb } = this.parsePath(request.path);
        const action =
          explicitAction ||
          `${(entityType || 'resource').toUpperCase()}_${
            (verb && VERB_ACTIONS[verb]) || METHOD_ACTIONS[request.method] || 'CHANGED'
          }`;

        const responseId = response?.data?.id;

        void this.auditService.record({
          userId: (request as any).user?.id,
          action,
          entityType,
          entityId: entityId ?? (responseId ? Number(responseId) : undefined),
          newValue: requestBody,
          ip: this.clientIp(request),
        });
      }),
    );
  }

  /** /api/v1/admin/modules/12/publish -> { entityType: 'module', entityId: 12, verb: 'publish' } */
  private parsePath(path: string): {
    entityType?: string;
    entityId?: number;
    verb?: string;
  } {
    const segments = path.split('/').filter(Boolean);
    const adminIndex = segments.indexOf('admin');
    const rest = segments.slice(adminIndex + 1);
    if (rest.length === 0) return {};

    const entityType = singularize(rest[0]);
    let entityId: number | undefined;
    let verb: string | undefined;

    for (const segment of rest.slice(1)) {
      if (/^\d+$/.test(segment)) {
        entityId = Number(segment);
      } else {
        verb = segment;
      }
    }

    return { entityType, entityId, verb };
  }

  private clientIp(request: Request): string | undefined {
    const forwarded = request.headers['x-forwarded-for'];
    if (typeof forwarded === 'string' && forwarded.length > 0) {
      return forwarded.split(',')[0].trim();
    }
    return request.ip;
  }
}
