import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service";
import { ActivityService } from "../activity/activity.service";
import { VaultService } from "../vault/vault.service";
import { getRlsClient } from "../prisma/rls-context.provider";
import { TransitKey } from "../vault/vault.constants";
import { ActivityAction, EntityType, Prisma } from "@prisma/client";
import type {
  CreateCredentialDto,
  UpdateCredentialDto,
  QueryCredentialDto,
  QueryCredentialAuditDto,
  CredentialResponseDto,
  PaginatedCredentialsDto,
  CredentialValueResponseDto,
} from "./dto";
import { CredentialScope } from "@prisma/client";

/**
 * Service for managing user credentials with encryption and RLS
 */
@Injectable()
export class CredentialsService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly activityService: ActivityService,
    private readonly vaultService: VaultService
  ) {}

  /**
   * Create a new credential
   * Encrypts value before storage and generates maskedValue
   */
  async create(
    workspaceId: string,
    userId: string,
    dto: CreateCredentialDto
  ): Promise<CredentialResponseDto> {
    const client = getRlsClient() ?? this.prisma;

    // Encrypt the credential value
    const encryptedValue = await this.vaultService.encrypt(dto.value, TransitKey.CREDENTIALS);

    // Generate masked value (last 4 characters)
    const maskedValue = this.generateMaskedValue(dto.value);

    // Create the credential
    const credential = await client.userCredential.create({
      data: {
        userId,
        workspaceId,
        name: dto.name,
        provider: dto.provider,
        type: dto.type,
        scope: dto.scope ?? CredentialScope.USER,
        encryptedValue,
        maskedValue,
        description: dto.description ?? null,
        expiresAt: dto.expiresAt ?? null,
        metadata: (dto.metadata ?? {}) as Prisma.InputJsonValue,
      },
      select: this.getSelectFields(),
    });

    // Log activity (fire-and-forget)
    await this.activityService.logActivity({
      workspaceId,
      userId,
      action: ActivityAction.CREDENTIAL_CREATED,
      entityType: EntityType.CREDENTIAL,
      entityId: credential.id,
      details: {
        name: credential.name,
        provider: credential.provider,
        type: credential.type,
      },
    });

    return credential as CredentialResponseDto;
  }

  /**
   * Get paginated credentials with filters
   * NEVER returns encryptedValue - only maskedValue
   */
  async findAll(
    workspaceId: string,
    userId: string,
    query: QueryCredentialDto
  ): Promise<PaginatedCredentialsDto> {
    const client = getRlsClient() ?? this.prisma;

    const page = query.page ?? 1;
    const limit = query.limit ?? 50;
    const skip = (page - 1) * limit;

    // Build where clause
    const where: Prisma.UserCredentialWhereInput = {
      userId,
      workspaceId,
    };

    if (query.type !== undefined) {
      where.type = {
        in: Array.isArray(query.type) ? query.type : [query.type],
      };
    }

    if (query.scope !== undefined) {
      where.scope = query.scope;
    }

    if (query.provider !== undefined) {
      where.provider = query.provider;
    }

    if (query.isActive !== undefined) {
      where.isActive = query.isActive;
    }

    if (query.search !== undefined) {
      where.OR = [
        { name: { contains: query.search, mode: "insensitive" } },
        { description: { contains: query.search, mode: "insensitive" } },
        { provider: { contains: query.search, mode: "insensitive" } },
      ];
    }

    // Execute queries in parallel
    const [data, total] = await Promise.all([
      client.userCredential.findMany({
        where,
        select: this.getSelectFields(),
        orderBy: { createdAt: "desc" },
        skip,
        take: limit,
      }),
      client.userCredential.count({ where }),
    ]);

    return {
      data: data as CredentialResponseDto[],
      meta: {
        total,
        page,
        limit,
        totalPages: Math.ceil(total / limit),
      },
    };
  }

  /**
   * Get a single credential by ID
   * NEVER returns encryptedValue - only maskedValue
   */
  async findOne(id: string, workspaceId: string, userId: string): Promise<CredentialResponseDto> {
    const client = getRlsClient() ?? this.prisma;

    const credential = await client.userCredential.findUnique({
      where: {
        id,
        userId,
        workspaceId,
      },
      select: this.getSelectFields(),
    });

    if (!credential) {
      throw new NotFoundException(`Credential with ID ${id} not found`);
    }

    return credential as CredentialResponseDto;
  }

  /**
   * Get decrypted credential value
   * Intentionally separate endpoint for security
   * Updates lastUsedAt and logs access
   */
  async getValue(
    id: string,
    workspaceId: string,
    userId: string
  ): Promise<CredentialValueResponseDto> {
    const client = getRlsClient() ?? this.prisma;

    // Fetch credential with encryptedValue
    const credential = await client.userCredential.findUnique({
      where: {
        id,
        userId,
        workspaceId,
      },
      select: {
        id: true,
        name: true,
        provider: true,
        encryptedValue: true,
        isActive: true,
        workspaceId: true,
      },
    });

    if (!credential) {
      throw new NotFoundException(`Credential with ID ${id} not found`);
    }

    if (!credential.isActive) {
      throw new BadRequestException("Cannot decrypt inactive credential");
    }

    // Decrypt the value
    const value = await this.vaultService.decrypt(
      credential.encryptedValue,
      TransitKey.CREDENTIALS
    );

    // Update lastUsedAt
    await client.userCredential.update({
      where: { id },
      data: { lastUsedAt: new Date() },
    });

    // Log access (fire-and-forget)
    await this.activityService.logActivity({
      workspaceId,
      userId,
      action: ActivityAction.CREDENTIAL_ACCESSED,
      entityType: EntityType.CREDENTIAL,
      entityId: id,
      details: {
        name: credential.name,
        provider: credential.provider,
      },
    });

    return { value };
  }

  /**
   * Update credential metadata
   * Cannot update the value itself - use rotate endpoint
   */
  async update(
    id: string,
    workspaceId: string,
    userId: string,
    dto: UpdateCredentialDto
  ): Promise<CredentialResponseDto> {
    const client = getRlsClient() ?? this.prisma;

    // Verify credential exists
    await this.findOne(id, workspaceId, userId);

    // Build update data object with only defined fields
    const updateData: Prisma.UserCredentialUpdateInput = {};
    if (dto.description !== undefined) {
      updateData.description = dto.description;
    }
    if (dto.expiresAt !== undefined) {
      updateData.expiresAt = dto.expiresAt;
    }
    if (dto.metadata !== undefined) {
      updateData.metadata = dto.metadata as Prisma.InputJsonValue;
    }
    if (dto.isActive !== undefined) {
      updateData.isActive = dto.isActive;
    }

    // Update credential
    const credential = await client.userCredential.update({
      where: { id },
      data: updateData,
      select: this.getSelectFields(),
    });

    // Log activity (fire-and-forget)
    await this.activityService.logActivity({
      workspaceId,
      userId,
      action: ActivityAction.UPDATED,
      entityType: EntityType.CREDENTIAL,
      entityId: id,
      details: {
        description: dto.description,
        expiresAt: dto.expiresAt?.toISOString(),
        isActive: dto.isActive,
      } as Prisma.JsonValue,
    });

    return credential as CredentialResponseDto;
  }

  /**
   * Rotate credential value
   * Encrypts new value and updates maskedValue
   */
  async rotate(
    id: string,
    workspaceId: string,
    userId: string,
    newValue: string
  ): Promise<CredentialResponseDto> {
    const client = getRlsClient() ?? this.prisma;

    // Verify credential exists
    const existing = await this.findOne(id, workspaceId, userId);

    // Encrypt new value
    const encryptedValue = await this.vaultService.encrypt(newValue, TransitKey.CREDENTIALS);

    // Generate new masked value
    const maskedValue = this.generateMaskedValue(newValue);

    // Update credential
    const credential = await client.userCredential.update({
      where: { id },
      data: {
        encryptedValue,
        maskedValue,
        rotatedAt: new Date(),
      },
      select: this.getSelectFields(),
    });

    // Log activity (fire-and-forget)
    await this.activityService.logActivity({
      workspaceId,
      userId,
      action: ActivityAction.CREDENTIAL_ROTATED,
      entityType: EntityType.CREDENTIAL,
      entityId: id,
      details: {
        name: existing.name,
        provider: existing.provider,
      },
    });

    return credential as CredentialResponseDto;
  }

  /**
   * Soft-delete credential (set isActive = false)
   */
  async remove(id: string, workspaceId: string, userId: string): Promise<void> {
    const client = getRlsClient() ?? this.prisma;

    // Verify credential exists
    const existing = await this.findOne(id, workspaceId, userId);

    // Soft delete
    await client.userCredential.update({
      where: { id },
      data: { isActive: false },
    });

    // Log activity (fire-and-forget)
    await this.activityService.logActivity({
      workspaceId,
      userId,
      action: ActivityAction.CREDENTIAL_REVOKED,
      entityType: EntityType.CREDENTIAL,
      entityId: id,
      details: {
        name: existing.name,
        provider: existing.provider,
      },
    });
  }

  /**
   * Get credential audit logs with filters and pagination
   * Returns all credential-related activities for the workspace
   * RLS ensures users only see their own workspace activities
   */
  async getAuditLog(
    workspaceId: string,
    query: QueryCredentialAuditDto
  ): Promise<{
    data: {
      id: string;
      action: ActivityAction;
      entityId: string;
      createdAt: Date;
      details: Record<string, unknown>;
      user: {
        id: string;
        name: string | null;
        email: string;
      };
    }[];
    meta: {
      total: number;
      page: number;
      limit: number;
      totalPages: number;
    };
  }> {
    const page = query.page ?? 1;
    const limit = query.limit ?? 20;
    const skip = (page - 1) * limit;

    // Build where clause
    const where: Prisma.ActivityLogWhereInput = {
      workspaceId,
      entityType: EntityType.CREDENTIAL,
    };

    // Filter by specific credential if provided
    if (query.credentialId) {
      where.entityId = query.credentialId;
    }

    // Filter by action if provided
    if (query.action) {
      where.action = query.action;
    }

    // Filter by date range if provided
    if (query.startDate || query.endDate) {
      where.createdAt = {};
      if (query.startDate) {
        where.createdAt.gte = query.startDate;
      }
      if (query.endDate) {
        where.createdAt.lte = query.endDate;
      }
    }

    // Execute queries in parallel
    const [data, total] = await Promise.all([
      this.prisma.activityLog.findMany({
        where,
        select: {
          id: true,
          action: true,
          entityId: true,
          createdAt: true,
          details: true,
          user: {
            select: {
              id: true,
              name: true,
              email: true,
            },
          },
        },
        orderBy: {
          createdAt: "desc",
        },
        skip,
        take: limit,
      }),
      this.prisma.activityLog.count({ where }),
    ]);

    return {
      data: data as {
        id: string;
        action: ActivityAction;
        entityId: string;
        createdAt: Date;
        details: Record<string, unknown>;
        user: {
          id: string;
          name: string | null;
          email: string;
        };
      }[],
      meta: {
        total,
        page,
        limit,
        totalPages: Math.ceil(total / limit),
      },
    };
  }

  /**
   * Get select fields for credential responses
   * NEVER includes encryptedValue
   */
  private getSelectFields(): Prisma.UserCredentialSelect {
    return {
      id: true,
      userId: true,
      workspaceId: true,
      name: true,
      provider: true,
      type: true,
      scope: true,
      maskedValue: true,
      description: true,
      expiresAt: true,
      lastUsedAt: true,
      metadata: true,
      isActive: true,
      rotatedAt: true,
      createdAt: true,
      updatedAt: true,
    };
  }

  /**
   * Generate masked value showing only last 4 characters
   */
  private generateMaskedValue(value: string): string {
    if (value.length <= 4) {
      return `****${value}`;
    }
    return `****${value.slice(-4)}`;
  }
}
