import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { GetObjectCommand } from '@aws-sdk/client-s3';
import { PrismaService } from '../infra/prisma.service';
import { AccessService } from './access.service';
import { ChatService } from './chat.service';
import { NotificationsService } from './notifications.service';
import { MinioService } from '../infra/minio.service';

const AVATAR_ACCESS_TTL_SECONDS = 900;

interface MediaStreamPayload {
  purpose: 'media-stream';
  objectKey: string;
  ownerId: string;
  roomId?: string;
  fileName?: string;
}

interface MediaUploadPayload {
  purpose: 'media-upload';
  storageKey: string;
  contentType: string;
  userId: string;
}

const UPLOAD_TTL_SECONDS = 300;

@Injectable()
export class MediaService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly minio: MinioService,
    private readonly jwt: JwtService,
    private readonly config: ConfigService,
    private readonly access: AccessService,
    private readonly chat: ChatService,
    private readonly notifications: NotificationsService
  ) {}

  async createAvatarUploadUrl(userId: string, contentType: string) {
    try {
      this.minio.assertImageContentType(contentType);
    } catch (error) {
      throw new BadRequestException(error instanceof Error ? error.message : 'Некорректный тип файла');
    }
    const extension = this.minio.extensionForContentType(contentType);
    const storageKey = `avatars/${userId}/${crypto.randomUUID()}.${extension}`;
    return this.createUploadTarget(userId, storageKey, contentType);
  }

  async confirmAvatar(userId: string, storageKey: string) {
    if (!storageKey.startsWith(`avatars/${userId}/`)) {
      throw new BadRequestException('Некорректный ключ аватара');
    }

    await this.prisma.user.update({
      where: { id: userId },
      data: {
        avatarStorageKey: storageKey,
        avatarUrl: null
      }
    });

    await this.publishUserAvatarUpdated(userId);

    return { userId, hasAvatar: true };
  }

  async getAvatarAccessUrl(requesterId: string, targetUserId: string) {
    const user = await this.prisma.user.findUnique({ where: { id: targetUserId } });
    if (!user?.avatarStorageKey) {
      throw new NotFoundException('Аватар не найден');
    }

    await this.assertCanViewUserAvatar(requesterId, targetUserId);

    const token = await this.jwt.signAsync(
      {
        purpose: 'media-stream',
        objectKey: user.avatarStorageKey,
        ownerId: targetUserId
      } satisfies MediaStreamPayload,
      {
        secret: this.config.getOrThrow<string>('JWT_ACCESS_SECRET'),
        expiresIn: AVATAR_ACCESS_TTL_SECONDS,
        issuer: 'id.lendry.ru'
      }
    );

    const publicApiUrl = this.config.get<string>('PUBLIC_API_URL', 'http://localhost:3000').replace(/\/$/, '');
    return {
      accessUrl: `${publicApiUrl}/media/stream/${token}`,
      expiresAt: new Date(Date.now() + AVATAR_ACCESS_TTL_SECONDS * 1000).toISOString()
    };
  }

  async createDocumentPhotoUploadUrl(userId: string, documentId: string, contentType: string) {
    try {
      this.minio.assertImageContentType(contentType);
    } catch (error) {
      throw new BadRequestException(error instanceof Error ? error.message : 'Некорректный тип файла');
    }
    const document = await this.prisma.userDocument.findFirst({
      where: { id: documentId, userId }
    });
    if (!document) {
      throw new NotFoundException('Документ не найден');
    }

    const extension = this.minio.extensionForContentType(contentType);
    const storageKey = `documents/${userId}/${documentId}/${crypto.randomUUID()}.${extension}`;
    return this.createUploadTarget(userId, storageKey, contentType);
  }

  async resolveStreamToken(token: string, requesterId?: string) {
    let payload: MediaStreamPayload;
    try {
      payload = await this.jwt.verifyAsync<MediaStreamPayload>(token, {
        secret: this.config.getOrThrow<string>('JWT_ACCESS_SECRET'),
        issuer: 'id.lendry.ru'
      });
    } catch {
      throw new ForbiddenException('Ссылка недействительна или истекла');
    }

    if (payload.purpose !== 'media-stream' || !payload.objectKey) {
      throw new ForbiddenException('Ссылка недействительна');
    }

    if (payload.objectKey.startsWith('chat/')) {
      if (!requesterId) {
        throw new ForbiddenException('Для доступа к файлу чата требуется авторизация');
      }
      const roomId = payload.roomId ?? this.extractChatRoomId(payload.objectKey);
      if (!roomId) {
        throw new ForbiddenException('Некорректный ключ файла чата');
      }
      await this.assertChatMember(roomId, requesterId);
    }

    const contentType = this.minio.contentTypeForKey(payload.objectKey);

    return {
      storageKey: payload.objectKey,
      contentType,
      fileName: payload.fileName
    };
  }

  async getObjectStream(storageKey: string) {
    return this.minio.getClient().send(
      new GetObjectCommand({
        Bucket: this.minio.getBucket(),
        Key: storageKey
      })
    );
  }

  async getDocumentPhotoAccessUrl(requesterId: string, targetUserId: string, storageKey: string) {
    await this.access.assertCanViewUserDocuments(requesterId, targetUserId);

    if (!storageKey.startsWith(`documents/${targetUserId}/`)) {
      throw new BadRequestException('Некорректный ключ фото документа');
    }

    const token = await this.jwt.signAsync(
      {
        purpose: 'media-stream',
        objectKey: storageKey,
        ownerId: targetUserId
      } satisfies MediaStreamPayload,
      {
        secret: this.config.getOrThrow<string>('JWT_ACCESS_SECRET'),
        expiresIn: AVATAR_ACCESS_TTL_SECONDS,
        issuer: 'id.lendry.ru'
      }
    );

    const publicApiUrl = this.config.get<string>('PUBLIC_API_URL', 'http://localhost:3000').replace(/\/$/, '');
    return {
      accessUrl: `${publicApiUrl}/media/stream/${token}`,
      expiresAt: new Date(Date.now() + AVATAR_ACCESS_TTL_SECONDS * 1000).toISOString()
    };
  }

  async createFamilyAvatarUploadUrl(requesterId: string, groupId: string, contentType: string) {
    await this.assertFamilyMember(groupId, requesterId);
    try {
      this.minio.assertImageContentType(contentType);
    } catch (error) {
      throw new BadRequestException(error instanceof Error ? error.message : 'Некорректный тип файла');
    }
    const extension = this.minio.extensionForContentType(contentType);
    const storageKey = `families/${groupId}/${crypto.randomUUID()}.${extension}`;
    return this.createUploadTarget(requesterId, storageKey, contentType);
  }

  async confirmFamilyAvatar(requesterId: string, groupId: string, storageKey: string) {
    await this.assertFamilyMember(groupId, requesterId);
    if (!storageKey.startsWith(`families/${groupId}/`)) {
      throw new BadRequestException('Некорректный ключ аватара семьи');
    }
    await this.prisma.familyGroup.update({
      where: { id: groupId },
      data: { avatarStorageKey: storageKey, hasAvatar: true }
    });
    return { groupId, hasAvatar: true };
  }

  async getFamilyAvatarAccessUrl(requesterId: string, groupId: string) {
    await this.assertFamilyMember(groupId, requesterId);
    const group = await this.prisma.familyGroup.findUnique({ where: { id: groupId } });
    if (!group?.avatarStorageKey) {
      throw new NotFoundException('Аватар семьи не найден');
    }
    return this.createStreamAccessUrl(group.avatarStorageKey, groupId);
  }

  async createChatMediaUploadUrl(requesterId: string, roomId: string, contentType: string, fileName?: string) {
    await this.assertChatMember(roomId, requesterId);
    let normalized: string;
    try {
      normalized = this.minio.assertChatMediaContentType(contentType, fileName);
    } catch (error) {
      throw new BadRequestException(error instanceof Error ? error.message : 'Некорректный тип файла');
    }
    const extension = this.minio.extensionForChatMedia(normalized, fileName);
    const storageKey = `chat/${roomId}/${crypto.randomUUID()}.${extension}`;
    return this.createUploadTarget(requesterId, storageKey, normalized);
  }

  async getChatMediaAccessUrl(requesterId: string, roomId: string, storageKey: string, fileName?: string) {
    await this.assertChatMember(roomId, requesterId);
    if (!storageKey.startsWith(`chat/${roomId}/`)) {
      throw new BadRequestException('Некорректный ключ медиафайла');
    }
    return this.createStreamAccessUrl(storageKey, requesterId, { roomId, fileName });
  }

  async createChatRoomAvatarUploadUrl(requesterId: string, roomId: string, contentType: string) {
    await this.assertChatMember(roomId, requesterId);
    try {
      this.minio.assertImageContentType(contentType);
    } catch (error) {
      throw new BadRequestException(error instanceof Error ? error.message : 'Некорректный тип файла');
    }
    const extension = this.minio.extensionForContentType(contentType);
    const storageKey = `chat-rooms/${roomId}/${crypto.randomUUID()}.${extension}`;
    return this.createUploadTarget(requesterId, storageKey, contentType);
  }

  async confirmChatRoomAvatar(requesterId: string, roomId: string, storageKey: string) {
    await this.assertChatMember(roomId, requesterId);
    if (!storageKey.startsWith(`chat-rooms/${roomId}/`)) {
      throw new BadRequestException('Некорректный ключ аватара чата');
    }
    await this.prisma.chatRoom.update({
      where: { id: roomId },
      data: { avatarStorageKey: storageKey, hasAvatar: true }
    });
    await this.chat.publishRoomAvatarChangedMessage(roomId, requesterId);
    return { roomId, hasAvatar: true };
  }

  async getChatRoomAvatarAccessUrl(requesterId: string, roomId: string) {
    await this.assertChatMember(roomId, requesterId);
    const room = await this.prisma.chatRoom.findUnique({ where: { id: roomId } });
    if (!room?.avatarStorageKey) {
      throw new NotFoundException('Аватар чата не найден');
    }
    return this.createStreamAccessUrl(room.avatarStorageKey, requesterId, { roomId });
  }

  private extractChatRoomId(storageKey: string) {
    const [prefix, roomId] = storageKey.split('/');
    if (prefix !== 'chat' || !roomId) {
      return null;
    }
    return roomId;
  }

  private extractChatRoomAvatarRoomId(storageKey: string) {
    const [prefix, roomId] = storageKey.split('/');
    if (prefix !== 'chat-rooms' || !roomId) {
      return null;
    }
    return roomId;
  }

  private async createUploadTarget(userId: string, storageKey: string, contentType: string) {
    const viaApi = this.config.get<string>('MINIO_UPLOAD_VIA_API', 'true') !== 'false';
    const expiresAt = new Date(Date.now() + UPLOAD_TTL_SECONDS * 1000).toISOString();

    if (viaApi) {
      const uploadToken = await this.jwt.signAsync(
        {
          purpose: 'media-upload',
          storageKey,
          contentType,
          userId
        } satisfies MediaUploadPayload,
        {
          secret: this.config.getOrThrow<string>('JWT_ACCESS_SECRET'),
          expiresIn: UPLOAD_TTL_SECONDS,
          issuer: 'id.lendry.ru'
        }
      );
      const publicApiUrl = this.config.get<string>('PUBLIC_API_URL', 'http://localhost:3001').replace(/\/$/, '');
      return {
        uploadUrl: `${publicApiUrl}/media/upload`,
        uploadToken,
        storageKey,
        expiresAt
      };
    }

    const presigned = await this.minio.createPresignedUploadUrl(storageKey, contentType, UPLOAD_TTL_SECONDS);
    return { ...presigned, storageKey };
  }

  private async createStreamAccessUrl(
    storageKey: string,
    ownerId: string,
    options?: { roomId?: string; fileName?: string }
  ) {
    const token = await this.jwt.signAsync(
      {
        purpose: 'media-stream',
        objectKey: storageKey,
        ownerId,
        roomId: options?.roomId,
        fileName: options?.fileName
      } satisfies MediaStreamPayload,
      {
        secret: this.config.getOrThrow<string>('JWT_ACCESS_SECRET'),
        expiresIn: AVATAR_ACCESS_TTL_SECONDS,
        issuer: 'id.lendry.ru'
      }
    );
    const publicApiUrl = this.config.get<string>('PUBLIC_API_URL', 'http://localhost:3000').replace(/\/$/, '');
    return {
      accessUrl: `${publicApiUrl}/media/stream/${token}`,
      expiresAt: new Date(Date.now() + AVATAR_ACCESS_TTL_SECONDS * 1000).toISOString()
    };
  }

  private async assertFamilyMember(groupId: string, userId: string) {
    const member = await this.prisma.familyMember.findFirst({ where: { groupId, userId } });
    if (!member) {
      throw new ForbiddenException('Нет доступа к семье');
    }
  }

  private async assertChatMember(roomId: string, userId: string) {
    const member = await this.prisma.chatRoomMember.findFirst({ where: { roomId, userId } });
    if (!member) {
      throw new ForbiddenException('Нет доступа к чату');
    }
  }

  private async publishUserAvatarUpdated(userId: string) {
    const memberships = await this.prisma.familyMember.findMany({
      where: { userId },
      select: { groupId: true }
    });
    const groupIds = memberships.map((member) => member.groupId);
    if (!groupIds.length) {
      return;
    }

    const coMembers = await this.prisma.familyMember.findMany({
      where: { groupId: { in: groupIds }, userId: { not: userId } },
      select: { userId: true }
    });
    const recipientIds = [...new Set(coMembers.map((member) => member.userId))];
    await Promise.all(
      recipientIds.map((memberId) =>
        this.notifications.publishRealtime(memberId, 'user_avatar_updated', '', '', { userId })
      )
    );
  }

  private async assertCanViewUserAvatar(requesterId: string, targetUserId: string) {
    if (requesterId === targetUserId) {
      return;
    }

    const requester = await this.prisma.user.findUnique({ where: { id: requesterId } });
    if (requester?.isSuperAdmin) {
      return;
    }

    const [requesterFamilies, targetFamilies] = await Promise.all([
      this.prisma.familyMember.findMany({ where: { userId: requesterId }, select: { groupId: true } }),
      this.prisma.familyMember.findMany({ where: { userId: targetUserId }, select: { groupId: true } })
    ]);
    const targetGroupIds = new Set(targetFamilies.map((member) => member.groupId));
    if (requesterFamilies.some((member) => targetGroupIds.has(member.groupId))) {
      return;
    }

    const sharedChat = await this.prisma.chatRoomMember.findFirst({
      where: {
        userId: requesterId,
        room: {
          members: {
            some: { userId: targetUserId }
          }
        }
      }
    });
    if (!sharedChat) {
      throw new ForbiddenException('Нет доступа к аватару пользователя');
    }
  }
}
