import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from "@nestjs/common";
import { Observable } from "rxjs";
import { tap } from "rxjs/operators";
import { ActivityService } from "../activity.service";
import { ActivityAction, EntityType } from "@prisma/client";
import type { Prisma } from "@prisma/client";
import type { CreateActivityLogInput } from "../interfaces/activity.interface";
import type { AuthenticatedRequest } from "../../common/types/user.types";

/**
 * Interceptor for automatic activity logging
 * Logs CREATE, UPDATE, DELETE actions based on HTTP methods
 */
@Injectable()
export class ActivityLoggingInterceptor implements NestInterceptor {
  private readonly logger = new Logger(ActivityLoggingInterceptor.name);

  constructor(private readonly activityService: ActivityService) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
    const request = context.switchToHttp().getRequest<AuthenticatedRequest>();
    const { method, user } = request;

    // Only log for authenticated requests
    if (!user) {
      return next.handle();
    }

    // Skip GET requests (read-only)
    if (method === "GET") {
      return next.handle();
    }

    return next.handle().pipe(
      tap((result: unknown): void => {
        // Use void to satisfy no-misused-promises rule
        void this.logActivity(context, request, result);
      })
    );
  }

  /**
   * Logs activity asynchronously (not awaited to avoid blocking response)
   */
  private async logActivity(
    context: ExecutionContext,
    request: AuthenticatedRequest,
    result: unknown
  ): Promise<void> {
    try {
      const { method, params, body, user, ip, headers } = request;

      if (!user) {
        return;
      }

      const action = this.mapMethodToAction(method);
      if (!action) {
        return;
      }

      // Extract entity information
      const resultObj = result as Record<string, unknown> | undefined;
      const entityId = params.id ?? (resultObj?.id as string | undefined);

      // workspaceId is now optional - log events even when missing
      const workspaceId = user.workspaceId ?? (body.workspaceId as string | undefined);

      // Log with warning if entityId is missing, but still proceed with logging if workspaceId exists
      if (!entityId) {
        this.logger.warn("Cannot log activity: missing entityId");
        return;
      }

      // Determine entity type from controller/handler
      const controllerName = context.getClass().name;
      const handlerName = context.getHandler().name;
      const entityType = this.inferEntityType(controllerName, handlerName);

      // Build activity details with sanitized body
      const sanitizedBody = this.sanitizeSensitiveData(body);
      const details: Prisma.JsonObject = {
        method,
        controller: controllerName,
        handler: handlerName,
      };

      if (method === "POST") {
        details.data = sanitizedBody;
      } else if (method === "PATCH" || method === "PUT") {
        details.changes = sanitizedBody;
      }

      // Extract user agent header
      const userAgentHeader = headers["user-agent"];
      const userAgent =
        typeof userAgentHeader === "string" ? userAgentHeader : userAgentHeader?.[0];

      // Log the activity — workspaceId is optional
      const activityInput: CreateActivityLogInput = {
        userId: user.id,
        action,
        entityType,
        entityId,
        details,
        ipAddress: ip ?? undefined,
        userAgent: userAgent ?? undefined,
      };
      if (workspaceId) {
        activityInput.workspaceId = workspaceId;
      }
      await this.activityService.logActivity(activityInput);
    } catch (error) {
      // Don't fail the request if activity logging fails
      this.logger.error(
        "Failed to log activity",
        error instanceof Error ? error.message : "Unknown error"
      );
    }
  }

  /**
   * Map HTTP method to ActivityAction
   */
  private mapMethodToAction(method: string): ActivityAction | null {
    switch (method) {
      case "POST":
        return ActivityAction.CREATED;
      case "PATCH":
      case "PUT":
        return ActivityAction.UPDATED;
      case "DELETE":
        return ActivityAction.DELETED;
      default:
        return null;
    }
  }

  /**
   * Infer entity type from controller/handler names
   */
  private inferEntityType(controllerName: string, handlerName: string): EntityType {
    const combined = `${controllerName} ${handlerName}`.toLowerCase();

    if (combined.includes("task")) {
      return EntityType.TASK;
    } else if (combined.includes("event")) {
      return EntityType.EVENT;
    } else if (combined.includes("project")) {
      return EntityType.PROJECT;
    } else if (combined.includes("workspace")) {
      return EntityType.WORKSPACE;
    } else if (combined.includes("user")) {
      return EntityType.USER;
    }

    // Default to TASK if cannot determine
    return EntityType.TASK;
  }

  /**
   * Sanitize sensitive data from objects before logging
   * Redacts common sensitive field names
   */
  private sanitizeSensitiveData(data: unknown): Prisma.JsonValue {
    if (typeof data !== "object" || data === null) {
      return data as Prisma.JsonValue;
    }

    // List of sensitive field names (case-insensitive)
    const sensitiveFields = [
      "password",
      "token",
      "secret",
      "apikey",
      "api_key",
      "authorization",
      "creditcard",
      "credit_card",
      "cvv",
      "ssn",
      "privatekey",
      "private_key",
    ];

    const sanitize = (obj: unknown): Prisma.JsonValue => {
      if (Array.isArray(obj)) {
        return obj.map((item) => sanitize(item)) as Prisma.JsonArray;
      }

      if (obj && typeof obj === "object") {
        const sanitized: Prisma.JsonObject = {};
        const objRecord = obj as Record<string, unknown>;

        for (const key in objRecord) {
          const lowerKey = key.toLowerCase();
          const isSensitive = sensitiveFields.some((field) => lowerKey.includes(field));

          if (isSensitive) {
            sanitized[key] = "[REDACTED]";
          } else if (typeof objRecord[key] === "object") {
            sanitized[key] = sanitize(objRecord[key]);
          } else {
            sanitized[key] = objRecord[key] as Prisma.JsonValue;
          }
        }

        return sanitized;
      }

      return obj as Prisma.JsonValue;
    };

    return sanitize(data);
  }
}
