import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Types } from 'mongoose';
import { assertAllowedMediaFile, MEDIA_MAX_SIZE_BYTES } from '../../common/media/allowed-media';
import { buildPaginatedResponse } from '../../common/utils/pagination.util';
import { resolveManagedFileUrl } from '../../common/utils/public-url.util';
import { resolveMongoSortDirection } from '../../common/utils/sort.util';
import { ManagedStorageService } from '../../infrastructure/storage/managed-storage.service';
import { NotificationsService } from '../notifications/notifications.service';
import { AssignSupportTicketDto } from './dto/assign-support-ticket.dto';
import { CreateSupportMessageDto } from './dto/create-support-message.dto';
import { CreateSupportTicketDto } from './dto/create-support-ticket.dto';
import { SupportTicketQueryDto } from './dto/support-ticket-query.dto';
import { UpdateSupportTicketStatusDto } from './dto/update-support-ticket-status.dto';
import { SupportRepository } from './support.repository';
import { SupportTicketDocument, SupportTicketStatus } from './schemas/support-ticket.schema';
import { SupportTicketMessageDocument } from './schemas/support-ticket-message.schema';

type UploadFile = {
  mimetype?: string;
  size: number;
  buffer: Buffer;
  originalname?: string;
};

@Injectable()
export class SupportService {
  private readonly logger = new Logger(SupportService.name);

  constructor(
    private readonly supportRepository: SupportRepository,
    private readonly storageService: ManagedStorageService,
    private readonly notificationsService: NotificationsService,
  ) {}

  async createTicket(userId: string, dto: CreateSupportTicketDto, image?: UploadFile) {
    this.assertValidUserId(userId);
    const subject = dto.subject.trim();
    const message = dto.message.trim();
    const attachmentUrl = await this.saveOptionalImage(image);
    const now = new Date();
    const ticket = await this.supportRepository.createTicket({
      userId,
      subject,
      category: dto.category ?? 'technical',
      priority: dto.priority ?? 'normal',
      lastMessageAt: now,
    });

    await this.supportRepository.createMessage({
      ticketId: ticket.id,
      senderId: userId,
      senderIdentifier: userId,
      senderRole: 'user',
      message,
      attachmentUrl,
      attachmentType: attachmentUrl ? 'image' : null,
      readByUser: true,
      readByAdmin: false,
    });

    return {
      message: 'Support ticket created successfully',
      item: this.mapTicket(ticket),
    };
  }

  async listMyTickets(userId: string, query: SupportTicketQueryDto) {
    this.assertValidUserId(userId);
    const page = query.page ?? 1;
    const limit = query.limit ?? 20;
    const skip = (page - 1) * limit;
    const filter = this.buildTicketFilter(query, { userId });
    const sort = { lastMessageAt: resolveMongoSortDirection(query.sortOrder) } as Record<string, 1 | -1>;

    const [items, total] = await Promise.all([
      this.supportRepository.findUserTickets(filter, skip, limit, sort),
      this.supportRepository.countTickets(filter),
    ]);

    return buildPaginatedResponse(
      items.map((ticket) => this.mapTicket(ticket)),
      { page, limit, total, offset: skip },
    );
  }

  async getMyTicket(userId: string, ticketId: string) {
    const ticket = await this.getOwnedTicketOrThrow(userId, ticketId);
    const messages = await this.supportRepository.findMessages(ticket.id);
    return {
      ticket: this.mapTicket(ticket),
      messages: messages.map((message) => this.mapMessage(message)),
    };
  }

  async addUserMessage(userId: string, ticketId: string, dto: CreateSupportMessageDto, image?: UploadFile) {
    const ticket = await this.getOwnedTicketOrThrow(userId, ticketId);
    if (ticket.status === 'closed') {
      throw new BadRequestException('Cannot add messages to a closed support ticket');
    }

    const attachmentUrl = await this.saveOptionalImage(image);
    const messageText = (dto.message ?? '').trim();
    this.assertMessageOrAttachment(messageText, attachmentUrl);

    const message = await this.supportRepository.createMessage({
      ticketId: ticket.id,
      senderId: userId,
      senderIdentifier: userId,
      senderRole: 'user',
      message: messageText,
      attachmentUrl,
      attachmentType: attachmentUrl ? 'image' : null,
      readByUser: true,
      readByAdmin: false,
    });
    await this.supportRepository.updateTicketAfterMessage(ticket.id, {});

    return {
      message: 'Support message added successfully',
      item: this.mapMessage(message),
    };
  }

  async closeMyTicket(userId: string, ticketId: string) {
    this.assertValidTicketId(ticketId);
    this.assertValidUserId(userId);
    const ticket = await this.supportRepository.closeTicket(ticketId, userId);
    if (!ticket) {
      throw new NotFoundException('Support ticket not found');
    }
    return {
      message: 'Support ticket closed successfully',
      item: this.mapTicket(ticket),
    };
  }

  async listAdminTickets(query: SupportTicketQueryDto) {
    const page = query.page ?? 1;
    const limit = query.limit ?? 20;
    const skip = (page - 1) * limit;
    const filter = this.buildTicketFilter(query);
    const sort = { lastMessageAt: resolveMongoSortDirection(query.sortOrder) } as Record<string, 1 | -1>;

    const [items, total] = await Promise.all([
      this.supportRepository.findAdminTickets(filter, skip, limit, sort),
      this.supportRepository.countTickets(filter),
    ]);

    return buildPaginatedResponse(
      items.map((ticket) => this.mapTicket(ticket)),
      { page, limit, total, offset: skip },
    );
  }

  async getAdminTicket(ticketId: string) {
    this.assertValidTicketId(ticketId);
    const ticket = await this.supportRepository.findAdminTicket(ticketId);
    if (!ticket) {
      throw new NotFoundException('Support ticket not found');
    }
    const messages = await this.supportRepository.findMessages(ticket.id);
    return {
      ticket: this.mapTicket(ticket),
      messages: messages.map((message) => this.mapMessage(message)),
    };
  }

  async addAdminMessage(
    adminIdentifier: string,
    ticketId: string,
    dto: CreateSupportMessageDto,
    image?: UploadFile,
  ) {
    this.assertValidTicketId(ticketId);
    const ticket = await this.supportRepository.findTicketById(ticketId);
    if (!ticket) {
      throw new NotFoundException('Support ticket not found');
    }
    if (ticket.status === 'closed') {
      throw new BadRequestException('Cannot add messages to a closed support ticket');
    }

    const attachmentUrl = await this.saveOptionalImage(image);
    const messageText = (dto.message ?? '').trim();
    this.assertMessageOrAttachment(messageText, attachmentUrl);
    const adminObjectId = Types.ObjectId.isValid(adminIdentifier) ? adminIdentifier : null;
    const message = await this.supportRepository.createMessage({
      ticketId: ticket.id,
      senderId: adminObjectId,
      senderIdentifier: adminIdentifier,
      senderRole: adminObjectId ? 'admin' : 'superadmin',
      message: messageText,
      attachmentUrl,
      attachmentType: attachmentUrl ? 'image' : null,
      readByUser: false,
      readByAdmin: true,
    });

    const nextStatus: Partial<{ status: SupportTicketStatus }> =
      ticket.status === 'open' ? { status: 'in_progress' } : {};
    await this.supportRepository.updateTicketAfterMessage(ticket.id, nextStatus);
    await this.notifyUserAboutAdminReply(adminObjectId, ticket.userId.toString(), ticket.id, messageText);

    return {
      message: 'Support reply sent successfully',
      item: this.mapMessage(message),
    };
  }

  async updateAdminStatus(ticketId: string, dto: UpdateSupportTicketStatusDto) {
    this.assertValidTicketId(ticketId);
    const ticket = await this.supportRepository.updateStatus(ticketId, dto.status);
    if (!ticket) {
      throw new NotFoundException('Support ticket not found');
    }
    return {
      message: 'Support ticket status updated successfully',
      item: this.mapTicket(ticket),
    };
  }

  async assignAdmin(ticketId: string, dto: AssignSupportTicketDto) {
    this.assertValidTicketId(ticketId);
    if (dto.adminId) {
      this.assertValidUserId(dto.adminId);
    }
    const ticket = await this.supportRepository.assign(ticketId, dto.adminId);
    if (!ticket) {
      throw new NotFoundException('Support ticket not found');
    }
    return {
      message: 'Support ticket assignment updated successfully',
      item: this.mapTicket(ticket),
    };
  }

  private buildTicketFilter(query: SupportTicketQueryDto, base?: { userId?: string }) {
    const filter: Record<string, unknown> = {};
    if (base?.userId) {
      filter.userId = new Types.ObjectId(base.userId);
    }
    if (query.userId) {
      filter.userId = new Types.ObjectId(query.userId);
    }
    if (query.status) {
      filter.status = query.status;
    }
    if (query.category) {
      filter.category = query.category;
    }
    if (query.priority) {
      filter.priority = query.priority;
    }
    return filter;
  }

  private async getOwnedTicketOrThrow(userId: string, ticketId: string) {
    this.assertValidTicketId(ticketId);
    this.assertValidUserId(userId);
    const ticket = await this.supportRepository.findUserTicket(ticketId, userId);
    if (!ticket) {
      throw new NotFoundException('Support ticket not found');
    }
    return ticket;
  }

  private async saveOptionalImage(file?: UploadFile): Promise<string> {
    if (!file) {
      return '';
    }
    const extension = assertAllowedMediaFile(file, 'image', {
      invalidMessage: 'Support attachment must be jpg, jpeg, png, or webp',
      maxSizeBytes: MEDIA_MAX_SIZE_BYTES.supportImage,
      maxSizeMessage: 'Support image must be 5MB or smaller',
    });

    return this.storageService.saveFile({
      folderSegments: ['support', 'images'],
      extension,
      buffer: file.buffer,
      contentType: file.mimetype,
      fileNamePrefix: 'support',
    });
  }

  private assertMessageOrAttachment(message: string, attachmentUrl: string) {
    if (!message && !attachmentUrl) {
      throw new BadRequestException('message or image is required');
    }
  }

  private assertValidTicketId(ticketId: string) {
    if (!Types.ObjectId.isValid(ticketId)) {
      throw new NotFoundException('Support ticket not found');
    }
  }

  private assertValidUserId(userId: string) {
    if (!Types.ObjectId.isValid(userId)) {
      throw new BadRequestException('Invalid user id');
    }
  }

  private mapTicket(ticket: SupportTicketDocument) {
    const object = ticket.toObject();
    const user = this.normalizePopulatedUser(object.userId);
    const assignedAdmin = this.normalizePopulatedUser(object.assignedAdminId);
    return {
      ...object,
      _id: object._id?.toString(),
      userId: user?._id ?? object.userId?.toString(),
      assignedAdminId: assignedAdmin?._id ?? object.assignedAdminId?.toString?.() ?? null,
      user,
      assignedAdmin,
    };
  }

  private mapMessage(message: SupportTicketMessageDocument) {
    const object = message.toObject();
    const sender = this.normalizePopulatedUser(object.senderId);
    return {
      ...object,
      _id: object._id?.toString(),
      ticketId: object.ticketId?.toString(),
      senderId: sender?._id ?? object.senderId?.toString?.() ?? null,
      sender,
      attachmentUrl: resolveManagedFileUrl(object.attachmentUrl),
    };
  }

  private normalizePopulatedUser(value: unknown) {
    if (!value || typeof value !== 'object') {
      return null;
    }
    const user = value as Record<string, any>;
    if (!user._id) {
      return null;
    }
    return {
      _id: user._id.toString(),
      name: user.name ?? '',
      username: user.username ?? '',
      stageName: user.stageName ?? '',
      avatar: resolveManagedFileUrl(user.avatar ?? ''),
      email: user.email ?? '',
      isDisabled: !!user.isDisabled,
      isVerified: !!user.isVerified,
    };
  }

  private async notifyUserAboutAdminReply(
    actorId: string | null,
    recipientId: string,
    ticketId: string,
    previewText: string,
  ) {
    if (!actorId || !Types.ObjectId.isValid(actorId)) {
      return;
    }
    try {
      await this.notificationsService.create({
        actorId,
        recipientId,
        type: 'support_reply',
        referenceId: ticketId,
        resourceType: 'support_ticket',
        deepLink: `/support/tickets/${ticketId}`,
        previewText: previewText.slice(0, 160),
      });
    } catch (error) {
      this.logger.warn(
        `Support reply notification failed for ticket=${ticketId}: ${
          error instanceof Error ? error.message : 'unknown error'
        }`,
      );
    }
  }
}
