import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import {
  CreateConversationDto,
  LikeDislikeConversationDto,
  UpdateConversationDto,
} from './chat-history.validation';
import { ChatHistory } from './chat-history.model';
import { InjectModel } from '@nestjs/mongoose';
import { Model, PipelineStage } from 'mongoose';
import { BINA_STREAM_RESPONSE_DIVIDER } from './chat-history.constant';
import { IFullUser } from '../users/users.interface';
import { SocketGuardsGateway } from 'src/shared/socket-guards/services/socket-guards.gateway';
import { StorageService } from '../storage/storage.service';
import { Writable } from 'stream';
import { Socket } from 'socket.io';
import moment from 'moment-timezone';
import { IMemoryItem } from '../users/users.interface';
import { ChatGptService } from 'src/shared/third-party/services/chat-gpt/chat-gpt.service';

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

  constructor(
    @InjectModel(ChatHistory.name)
    private readonly chatHistoryModel: Model<ChatHistory>,
    private readonly socketGateway: SocketGuardsGateway,
    private readonly storageService: StorageService,
    private readonly chatGptService: ChatGptService,
  ) {}

  async findAllConversation(
    user: IFullUser,
    query: Record<string, unknown>,
  ): Promise<{
    data: ChatHistory[];
    meta: { total: number; page: number; limit: number };
  }> {
    // if(user?.subscription?.plan === 'free') {
    // 	throw new BadRequestException('You need to upgrade to a premium plan to access this feature');
    // }

    const page = query.page ? parseInt(query.page as string) : 1;
    const limit = query.limit ? parseInt(query.limit as string) : 30;

    // Define date format for consistency
    const dateFormat = { format: '%Y-%m-%d', timezone: 'UTC' };

    // Helper function for date calculations
    const getDateAgo = (daysAgo: number) =>
      new Date(new Date().setDate(new Date().getDate() - daysAgo));

    const pipeline: PipelineStage[] = [
      {
        $project: {
          _id: 1,
          title: 1,
          favorite: 1,
          updatedAt: 1,
          dateRange: {
            $cond: {
              // updatedAt format: 2021-08-25T12:00:00.000Z, today format: 234234234234
              if: {
                $eq: [
                  { $dateToString: { date: '$updatedAt', ...dateFormat } },
                  { $dateToString: { date: new Date(), ...dateFormat } },
                ],
              },
              then: 'D. TODAY',
              else: {
                $cond: {
                  if: {
                    $eq: [
                      { $dateToString: { date: '$updatedAt', ...dateFormat } },
                      { $dateToString: { date: getDateAgo(1), ...dateFormat } },
                    ],
                  },
                  then: 'C. YESTERDAY',
                  else: {
                    // last 7 days
                    $cond: {
                      if: { $gte: ['$updatedAt', getDateAgo(7)] },
                      then: 'B. LAST7DAYS',
                      else: {
                        // last 30 days
                        $cond: {
                          if: { $gte: ['$updatedAt', getDateAgo(30)] },
                          then: 'A. LAST30DAYS',
                          else: {
                            // by month year (except last 30 days) - for rest of the documents
                            $dateToString: {
                              date: '$updatedAt',
                              format: '%Y-%m',
                              timezone: 'UTC',
                            },
                          },
                        },
                      },
                    },
                  },
                },
              },
            },
          },
        },
      },
      {
        $group: {
          _id: '$dateRange',
          data: {
            $push: {
              _id: '$_id',
              title: '$title',
              favorite: '$favorite',
              updatedAt: '$updatedAt',
            },
          },
          count: { $sum: 1 },
        },
      },
      {
        $sort: {
          _id: -1,
        },
      },
      {
        $project: {
          date: '$_id',
          _id: 0,
          data: '$data',
          count: '$count',
        },
      },
    ];

    // Default scope for the classic chat list is type='chat' (legacy rows often
    // have no `type` field at all, hence the $or). Pass `mode=unified` to switch
    // the listing to unified-chat conversations instead.
    const wantUnified = query.mode === 'unified';
    const matchStage: PipelineStage[] = [
      {
        $match: {
          user: query.user,
          ...(query.favorite && { favorite: query.favorite === 'true' }),
          ...(wantUnified
            ? { type: 'unified' }
            : { $or: [{ type: 'chat' }, { type: { $exists: false } }, { type: null }] }),
        },
      },
    ];

    const facetPipeline: any = [
      ...matchStage,
      {
        $sort: {
          updatedAt: -1,
        },
      },
      {
        $facet: {
          data: [
            {
              $skip: (page - 1) * limit,
            },
            {
              $limit: limit,
            },
            ...pipeline,
          ],
          count: [{ $count: 'count' }],
        },
      },
    ];

    const res = await this.chatHistoryModel.aggregate(facetPipeline);
    const data = res?.[0]?.data;
    const meta = {
      total: res?.[0]?.count?.[0]?.count || 0,
      page,
      limit,
      totalPages: Math.ceil((res?.[0]?.count?.[0]?.count || 0) / limit),
      hasMorePages: page * limit < (res?.[0]?.count?.[0]?.count || 0),
      nextPage:
        page * limit < (res?.[0]?.count?.[0]?.count || 0) ? page + 1 : null,
      previousPage: page > 1 ? page - 1 : null,
    };

    return {
      data,
      meta,
    };
  }

  async findConversationById(
    conversationId: string,
    user: IFullUser,
  ): Promise<ChatHistory> {
    // add blockedReason to the response if the user is admin
    const conversation = await this.chatHistoryModel
      .findOne({
        _id: conversationId,
        user: user._id,
      })
      .select(
        user.role !== 'user'
          ? '+history.blockedReason +history.parts.blockedReason'
          : '-history.blockedReason -history.parts.blockedReason',
      );

    if (!conversation) {
      throw new NotFoundException('Conversation not found');
    }

    const conversationObject = conversation.toObject();

    return this._addMemoriesToConversation(conversationObject, user.memory);
  }

  private _addMemoriesToConversation(
    conversation: Record<string, any>,
    userMemory: IMemoryItem[],
  ): ChatHistory {
    if (!userMemory || userMemory.length === 0) {
      return conversation as ChatHistory;
    }

    const memoryMap = new Map<string, IMemoryItem>();
    for (const mem of userMemory) {
      if (mem.messageId) {
        memoryMap.set(mem.messageId.toString(), mem);
      }
    }

    if (conversation.history && conversation.history.length > 0) {
      for (const message of conversation.history) {
        const messageIdStr = (message._id as any).toString();
        if (memoryMap.has(messageIdStr)) {
          (message as any).memory = memoryMap.get(messageIdStr);
        }
      }
    }

    return conversation as ChatHistory;
  }

  async updateConversation(
    conversationId: string,
    updateConversationDto: UpdateConversationDto,
  ): Promise<ChatHistory> {
    const result = await this.chatHistoryModel.findByIdAndUpdate(
      conversationId,
      updateConversationDto,
      { new: true, runValidators: true },
    );
    if (!result) {
      throw new NotFoundException('Conversation not found');
    }
    return result;
  }

  // Handle like dislike
  async handleLikeDislike(
    payload: LikeDislikeConversationDto,
    user: IFullUser,
  ) {
    const { like, dislike, conversationId, messageId, partId } = payload;

    // find message with message id and conversation id (user Id also)
    const conversation = await this.chatHistoryModel.findOne({
      _id: conversationId,
      user: user._id,
      'history._id': messageId,
    });

    if (!conversation) {
      throw new NotFoundException('Conversation not found');
    }

    if (partId) {
      const findPart = conversation.history
        .find((message) => message._id?.toString() === messageId)
        ?.parts?.find((part) => part._id?.toString() === partId);

      if (!findPart) {
        throw new NotFoundException('Part not found');
      }

      if (like !== undefined && like !== null) {
        findPart.like = like;
      }

      if (dislike !== undefined && dislike !== null) {
        findPart.dislike = dislike;
      }

      await conversation.save();
    } else {
      await this.chatHistoryModel.updateOne(
        { _id: conversationId, user: user._id, 'history._id': messageId },
        {
          $set: {
            // set like only if the like is true or false
            ...(like !== undefined &&
              like !== null && { 'history.$.like': like }),
            // set dislike only if the dislike is true or false
            ...(dislike !== undefined &&
              dislike !== null && { 'history.$.dislike': dislike }),
          },
        },
      );
    }
  }

  async deleteConversationById(conversationId: string, user: IFullUser) {
    const result =
      await this.chatHistoryModel.findByIdAndDelete(conversationId);
    if (!result) {
      throw new NotFoundException('Conversation not found');
    }

    // delete all images related to this conversation
    await this.storageService.deleteFilesById(
      {
        conversationId: conversationId,
      },
      user,
    );

    return result;
  }

  /**
   * Appends a unified-chat image message pair (user prompt + assistant images)
   * to a conversation, creating the conversation if `conversationId` is missing.
   * Used by the Unified Chat after a successful image generation so the image
   * shows up in the conversation history alongside text messages.
   */
  async appendImageMessage(
    user: IFullUser,
    payload: {
      conversationId?: string;
      prompt: string;
      images: string[];
      imageHistoryId?: string;
      model?: string;
      aspectRatio?: string;
      style?: string;
    },
  ): Promise<{ conversationId: string }> {
    const { conversationId, prompt, images, imageHistoryId, model, aspectRatio, style } = payload;

    const userMessage: any = {
      role: 'user',
      type: 'text',
      content: prompt,
      model,
    };
    const assistantMessage: any = {
      role: 'system',
      type: 'image',
      content: '',
      images: (images || []).map(url => ({ url })),
      model,
      // Cross-reference to the dedicated image-history record (used for delete / regen flows).
      ...(imageHistoryId ? { metadata: { imageHistoryId, aspectRatio, style } } : {}),
    };

    if (conversationId) {
      const existing = await this.chatHistoryModel.findOne({ _id: conversationId, user: user._id });
      if (existing) {
        existing.history.push(userMessage, assistantMessage);
        await existing.save();
        return { conversationId: existing._id.toString() };
      }
      // conversationId provided but not found / not owned — fall through and create a new one.
    }

    const created = await this.chatHistoryModel.create({
      user: user._id,
      title: (prompt || 'New image').slice(0, 80),
      type: 'unified',
      history: [userMessage, assistantMessage],
    });
    return { conversationId: created._id.toString() };
  }

  async deleteAllConversations(userId: string) {
    const result = await this.chatHistoryModel.deleteMany({ user: userId });
    if (!result?.deletedCount) {
      throw new NotFoundException('No conversations found to delete');
    }

    // delete all images related to this conversation
    await this.storageService.deleteFilesById(
      {
        userId: userId,
      },
      {
        _id: userId,
      },
    );

    return null;
  }

  private async streamText(
    createChatDto: CreateConversationDto,
    conversationId: string,
    streamText: string,
    res: Writable,
    user: IFullUser,
  ) {
    const chunks = streamText.split(' ');

    const clientSocket: Socket | undefined = [
      ...this.socketGateway.server.sockets.sockets.values(),
    ].find((socket: Socket) => {
      const socketUserId = socket.handshake.query.userId as string;
      const expectedUserId = user?._id.toString();

      return socketUserId === expectedUserId;
    });

    const abortController = new AbortController();

    clientSocket?.on('stop_stream', () => {
      abortController.abort();
      res.end(() => {
        this.logger.log('Stream Ended');
      });

      clientSocket.removeAllListeners('stop_stream');
    });

    for await (const chunk of chunks) {
      if (abortController.signal.aborted) break;

      res.write(`${chunk} `);

      await new Promise((resolve) => setTimeout(resolve, 25)); // Adjust the delay as needed
    }

    res.write(
      `${BINA_STREAM_RESPONSE_DIVIDER}${conversationId}${BINA_STREAM_RESPONSE_DIVIDER}`,
    );
  }

  // for admin. getting all blocked conversations
  async getAllBlockedConversations(query: Record<string, unknown>) {
    // handle page and limit
    const page = query.page ? parseInt(query.page as string) : 1;
    const limit = query.limit ? parseInt(query.limit as string) : 10;
    const { searchTerm, blockedReason, createdAt } = query;

    const matchStage: any = {
      $or: [
        // ...(searchTerm && [
        //   { title: { $regex: searchTerm, $options: 'i' } },
        //   { 'history.blockedReason': { $regex: searchTerm, $options: 'i' } },
        // ]),
        {
          history: {
            $elemMatch: {
              blockedReason: { $exists: true, $not: { $size: 0 } },
            },
          },
        },
        {
          history: {
            $elemMatch: {
              parts: {
                $elemMatch: {
                  blockedReason: { $exists: true, $not: { $size: 0 } },
                },
              },
            },
          },
        },
      ],
    };

    // Apply dynamic filters
    if (searchTerm) {
      matchStage['$or'] = [
        { title: { $regex: searchTerm, $options: 'i' } },
        { 'history.blockedReason': { $regex: searchTerm, $options: 'i' } },
      ];
    }

    if (blockedReason) {
      matchStage['history.blockedReason'] = {
        $regex: blockedReason,
        $options: 'i',
      };
    }
    if (createdAt) {
      const dateStart = moment(new Date(createdAt as any))
        .startOf('day')
        .toDate();
      const dateEnd = moment(new Date(createdAt as any))
        .endOf('day')
        .toDate();
      matchStage['createdAt'] = { $gte: dateStart, $lte: dateEnd };
    }

    const pipeline: PipelineStage[] = [
      { $match: matchStage },
      { $sort: { createdAt: -1 } },
    ];
    const response = await this.chatHistoryModel.aggregate([
      ...pipeline,
      {
        $facet: {
          data: [
            {
              $skip: (page - 1) * limit,
            },
            {
              $limit: limit,
            },
          ],
          count: [{ $count: 'count' }],
        },
      },
    ]);

    const totalCount = response[0]?.count?.[0]?.count || 0;
    const totalPages = Math.ceil(totalCount / limit);
    const hasMorePages = page * limit < totalCount;
    const nextPage = hasMorePages ? page + 1 : null;
    const previousPage = page > 1 ? page - 1 : null;

    return {
      data: response?.[0].data || [],
      meta: {
        total: totalCount,
        page,
        limit,
        totalPages,
        hasMorePages,
        nextPage,
        previousPage,
      },
    };
  }

  async transcribeAudio(audioFile: any, user: IFullUser): Promise<{ text: string }> {
    try {
      this.logger.log(`ChatHistoryService: transcribeAudio - transcribing audio for user: ${user.email}`);
      
      const text = await this.chatGptService.transcribeAudio(audioFile);
      
      this.logger.log(`ChatHistoryService: transcribeAudio - transcription completed successfully`);
      return { text };
    } catch (error) {
      this.logger.error(`ChatHistoryService: transcribeAudio failed - ${error?.message || error}`, error?.stack);
      throw error;
    }
  }
}
