import { HttpException, Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { Client, TextChannel } from 'discord.js';
import axios from 'axios';
import { ConfigService } from '@nestjs/config';
import * as nodemailer from 'nodemailer';

interface AttendanceRecord {
  id: string;
  personId: string;
  type: string;
  time: string;
  person?: { fullName: string };
  activity?: { name: string };
  note?: string;
}

@Injectable()
export class DiscordBotService implements OnModuleInit {
  private readonly logger = new Logger(DiscordBotService.name);
  private readonly transporter: nodemailer.Transporter;
  private discordClient: Client;
  private recentRecords: AttendanceRecord[] = [];
  private accessToken: string;
  private disableCronJobs: boolean;
  private _isCheckingAttendance: boolean = false;

  constructor(private readonly configService: ConfigService) {
    this.transporter = nodemailer.createTransport({
      host: this.configService.get<string>('DISCORD_BOT_SMTP_HOST'),
      port: this.configService.get<number>('DISCORD_BOT_SMTP_PORT'),
      secure: true,
      auth: {
        user: this.configService.get<string>('DISCORD_BOT_SMTP_USER'),
        pass: this.configService.get<string>('DISCORD_BOT_SMTP_PASSWORD'),
      },
    });

    this.disableCronJobs = this.configService.get('DISABLE_CRON_JOBS') === 'true';
    this.logger.log('Cron jobs disabled:', this.disableCronJobs);
    if (!this.disableCronJobs) {
      this.initializeDiscordBot();
    }
  }

  async onModuleInit() {
    if (this.disableCronJobs) {
      this.logger.log('Cron jobs are disabled. Skipping Discord bot initialization.');
    }
  }

  private async initializeDiscordBot() {
    this.discordClient = new Client({
      intents: ['Guilds', 'GuildMessages', 'MessageContent'],
    });

    try {
      await this.discordClient.login(this.configService.get<string>('DISCORD_BOT_TOKEN'));
      this.logger.log('Discord bot connected successfully');
    } catch (error) {
      this.sendEmail('Discord Bot Connection Failed', 'Failed to connect to Discord bot', new Date());
      this.logger.error('Discord bot connection failed:', error);
    }
  }

  private isRecordDuplicate(record: AttendanceRecord): boolean {
    return this.recentRecords.some(
      existingRecord =>
        existingRecord.personId === record.personId &&
        existingRecord.type === record.type &&
        new Date(existingRecord.time).getTime() === new Date(record.time).getTime(),
    );
  }

  private addToRecentRecords(record: AttendanceRecord) {
    // Add the new record
    this.recentRecords.push(record);

    // Keep only the last 10 records
    if (this.recentRecords.length > 10) {
      this.recentRecords.shift();
    }
  }

  private async getAccessToken() {
    try {
      const JibbleAccessTokenUrl = this.configService.get<string>('JIBBLE_ACCESS_TOKEN_URL');
      const response = await axios.post(
        JibbleAccessTokenUrl,
        new URLSearchParams({
          grant_type: 'client_credentials',
          client_id: this.configService.get<string>('JIBBLE_API_KEY_ID'),
          client_secret: this.configService.get<string>('JIBBLE_API_SECRET'),
        }),
        { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } },
      );

      if (!response.data || !response.data.access_token) {
        this.logger.error('Failed to fetch access token:', response.data);
        return new HttpException('Failed to fetch access token', 500);
      }

      this.accessToken = response.data.access_token;
      this.logger.log('Successfully obtained a new access token');
    } catch (error: any) {
      this.sendEmail('Failed to fetch access token', error?.message, new Date());
      this.logger.error('Failed to fetch access token:', error);
      return new HttpException('Failed to fetch access token', 500);
    }
  }

  @Cron(CronExpression.EVERY_MINUTE)
  async checkAttendance() {
    if (this.disableCronJobs) {
      this.logger.log('Cron job skipped due to development mode.');
      return;
    }

    if (this._isCheckingAttendance) {
      this.logger.warn('Previous attendance check still running, skipping this cycle');
      return;
    }

    this._isCheckingAttendance = true;

    try {
      if (!this.accessToken) {
        await this.getAccessToken();
      }

      // Calculate the datetime 3 minutes ago
      const threeMinutesAgo = new Date(Date.now() - 3 * 60 * 1000);
      const fromDate = threeMinutesAgo.toISOString();

      this.logger.debug(`Checking attendance from ${threeMinutesAgo.toLocaleString()}`);
      const JibbleUrl = this.configService.get<string>('JIBBLE_API_URL');

      const response = await axios.get(JibbleUrl, {
        headers: {
          Authorization: `Bearer ${this.accessToken}`,
        },
        params: {
          $count: true,
          $expand: 'activity($select=id,name,code),project($select=id,name,code),person($select=id,groupId,fullname)',
          $filter: `(time gt ${fromDate} and status ne 'Archived')`,
          $orderby: 'time asc',
          $top: 100,
          $select: 'id,type,time,localTime,belongsToDate,personId,projectId,activityId,locationId,note,coordinates,address',
        },
      });

      if (response.data && response.data.value.length > 0) {
        const records = response.data.value;

        for (const record of records) {
          // Check if the record is a duplicate
          if (this.isRecordDuplicate(record)) {
            this.logger.debug(`Skipping duplicate record: ${record.id}`);
            continue;
          }

          // Add to recent records
          this.addToRecentRecords(record);
          this.logger.log('recentRecords:', this.recentRecords);
          this.logger.log(`Sending notification for: ${record.person?.fullName} - ${record.type} at ${new Date(record.time).toLocaleTimeString()}`);

          await this.sendDiscordNotification(record);
        }
      }

    } catch (error) {
      if (error.response && error.response.status === 401) {
        this.logger.warn('Access token expired. Fetching a new token...');
        await this.getAccessToken();
      } else {
        this.logger.error('Failed to fetch attendance:', error);
        this.sendEmail('Failed to fetch attendance', error?.message, new Date());
        throw error;
      }
    } finally {
      this._isCheckingAttendance = false;
    }
  }

  private async sendDiscordNotification(record: AttendanceRecord) {
    try {
      const personName = record.person?.fullName || 'Unknown';
      const projectName = record.activity?.name || '';
      const note = record.note?.trim() || '';

      let clockAction = '';
      if (record.type === 'In') {
        clockAction = '_Clocked **In**_ 🕒';
      } else if (record.type === 'Out') {
        clockAction = '_Clocked **Out**_ ⏰';
      } else if (record.type === 'StartBreak') {
        clockAction = '_Clocked **StartBreak**_ ';
      }

      const message = `**${personName}** just → ${clockAction}\n> Working with  \`「${projectName}」\`\n> **Note** _${note}_`;

      const channel = await this.discordClient.channels.fetch(this.configService.get<string>('DISCORD_CHANNEL_ID'));
      if (channel.isTextBased()) {
        await (channel as TextChannel).send(message).then(async () => {
          await this.sendEmail('Discord Notification Sent', message, new Date());
        });
      }
    } catch (error) {
      this.sendEmail('Failed to send Discord notification', error?.message, new Date());
      this.logger.error('Failed to send Discord notification:', error);
      throw error;
    }
  }

  private async sendEmail(subject: string, message: string, date: Date) {
    const mailOptions = {
      from: this.configService.get<string>('DISCORD_BOT_SMTP_USER'),
      to: this.configService.get<string>('DISCORD_BOT_SMTP_USER'),
      subject,
      html: `<p>${message}</p><p><small>Sent at ${date.toLocaleString()}</small></p>`,
    };

    await this.transporter.sendMail(mailOptions).catch(error => {
      this.logger.error('Failed to send email:', error);
    });
  }
}
