import path from 'path';
import Hyperswarm, { PeerDiscovery } from 'hyperswarm';
import {TextMessage} from "./message/TextMessage";
import {FileMessage} from "./message/FileMessage";
import {AudioMessage} from "./message/AudioMessage";
import {Message} from "./message/Message";
import Corestore from 'corestore';
import Hyperdrive from 'hyperdrive';
import fs from 'fs';
// @ts-ignore
import ServeDrive from 'serve-drive';
import {IconMessage} from "./message/IconMessage";
import {TypedEventEmitter} from "./util/TypedEventEmitter";
import {LinkUpEvents} from "./LinkUpEvents";
import {Command} from "./Command";
import { glob } from "glob";
import { normalize } from "path";
import { promisify } from "util";

/**
 * This class is the core component of the bot system. It handles connections to the Hyperswarm network, manages message sending and receiving, and emits events for various actions.
 */
export class Client extends TypedEventEmitter<LinkUpEvents> {
  public botName: string = "";
  public servePort: number | null = 0;
  public storagePath: string | undefined;
  public swarm: Hyperswarm | undefined;
  public drive: Hyperdrive | undefined;
  public store: Corestore | undefined;
  public joinedRooms: Set<string> | undefined;
  public currentTopic: string | null = null;
  public botAvatar: string = "";
  public iconMessage: IconMessage | undefined;
  public discovery: PeerDiscovery | undefined;
  public commands: Command[] = []
  private commandPrefix: string;

  /**
   * @param botName The name of the bot.
   * @param commandPrefix Prefix for bot commands
   * @since 1.0
   * @constructor
   * @author snxraven
   */
  constructor(botName: string, commandPrefix: string) {
    super();
    this.botName = botName;
    this.commandPrefix = commandPrefix;
    this.swarm = new Hyperswarm();
    this.joinedRooms = new Set(); // Track the rooms the bot has joined
    this.currentTopic = null; // Track the current topic

    // Initialize Corestore and Hyperdrive
    this.storagePath = './storage/';
    this.store = new Corestore(this.storagePath);
    this.drive = new Hyperdrive(this.store);

    // Initialize ServeDrive
    this.servePort = null;
    this.initializeServeDrive();

    this.setupSwarm();

    process.on('exit', () => {
      console.log('EXIT signal received. Shutting down HyperSwarm...');
      this.destroy();
    });

    process.on('SIGTERM', async () => {
      console.log('SIGTERM signal received. Shutting down HyperSwarm...');
      await this.destroy();
      console.log('HyperSwarm was shut down. Exiting the process with exit code 0.');
      process.exit(0);
    });

    process.on('SIGINT', async () => {
      console.log('SIGINT signal received. Shutting down HyperSwarm...');
      await this.destroy();
      console.log('HyperSwarm was shut down. Exiting the process with exit code 0.');
      process.exit(0);
    });

    this.on("onMessage", (msg: TextMessage) => {
      const message = msg.message;
      if(!message.startsWith(this.commandPrefix)) return;
      const [commandName, ...args] = message.slice(this.commandPrefix.length).split(' ');

      const command = this.commands.find(c => c.options.name === commandName || c.options.aliases?.indexOf(commandName) !== -1);
      if(command) {
        console.log(`Executing command: ${command.options.name} (${command.options.aliases?.join(", ")}) with arguments: [${args.join(", ")}]`);
        command.handler(this, msg, args);
      } else {
        console.warn(`Command not found: ${command}`);
      }
    })
  }

  /**
   * @description Initializes the ServeDrive for serving files and audio.
   * @since 1.0
   * @author snxraven
   */
  async initializeServeDrive() {
    try {
      this.servePort = this.getRandomPort();
      const serve = new ServeDrive({
        port: this.servePort,
        // @ts-ignore
        get: ({ key, filename, version }) => this.drive
      });
      await serve.ready();
      console.log('ServeDrive listening on port:', this.servePort);
    } catch (error) {
      console.error('Error initializing ServeDrive:', error);
    }
  }
  /**
   * @description Returns a random port number.
   * @since 1.0
   * @author snxraven
   * @return Random port number.
   */
  getRandomPort(): number {
    return Math.floor(Math.random() * (65535 - 49152 + 1)) + 49152;
  }

  /**
   * @description Fetches and sets the bot's avatar from a local file.
   * @param filePath path to the local avatar file.
   * @since 1.0
   * @author snxraven
   */
  async fetchAvatar(filePath: string) {
    try {
      await this.drive?.ready();
      const iconBuffer = fs.readFileSync(filePath);
      await this.drive?.put(`/icons/${this.botName}.png`, iconBuffer);
      this.botAvatar = `http://localhost:${this.servePort}/icons/${this.botName}.png`;

      // Cache the icon message
      this.iconMessage = IconMessage.new(this, iconBuffer);
    } catch (error) {
      console.error('Error fetching avatar:', error);
    }
  }

  /**
   * @description Sets up the Hyperswarm network and connection handlers.
   * @since 1.0
   * @author snxraven
   */
  setupSwarm() {
    this.swarm?.on('connection', (peer) => {
      // Send the cached icon message to the new peer
      if (this.iconMessage) {
        peer.write(this.iconMessage.toJsonString());
      }

      peer.on('data', async (message: {}) => {
        const messageObj = JSON.parse(message.toString());
        if (this.joinedRooms?.has(messageObj.topic)) { // Process message only if it is from a joined room
          this.currentTopic = messageObj.topic; // Set the current topic from the incoming message

          const msgType = messageObj.type;
          const peerName = messageObj.name; // Changed from name to userName
          const peerAvatar = messageObj.avatar;
          const timestamp = messageObj.timestamp;


          if (msgType === "message")
            this.emit('onMessage', new TextMessage(peerName, peerAvatar, this.currentTopic, timestamp, messageObj.message));

          if (msgType === "file") {
            const fileBuffer = await this.drive?.get(`/files/${messageObj.fileName}`);
            /**
             * Triggered when a file message is received.
             *
             * @event Client#onFile
             * @property peer - HyperSwarm peer object
             * @property {FileMessage} FileMessage - Class with all of the information about received file
             * @example
             * const bot = new Client("MyBot");
             * bot.on('onFile', (peer, message) => {
             *   console.log(`Received file from ${message.peerName}`);
             * });
             */
            this.emit('onFile', new FileMessage(peerName, peerAvatar, this.currentTopic, timestamp, messageObj.fileName, `http://localhost:${this.servePort}/files/${messageObj.fileName}`, messageObj.fileType, messageObj.fileData));
          }

          if (msgType === "icon")
            /**
             * Triggered when an icon message is received.
             *
             * @event Client#onIcon
             * @property peer - HyperSwarm peer object
             * @property {IconMessage} IconMessage - Class with all of the information about received peer icon
             * @example
             * const bot = new Client("MyBot");
             * bot.on('onIcon', (peer, message) => {
             *   console.log(`Received new Icon from ${message.peerName}`);
             * });
             */
            this.emit('onIcon', new IconMessage(peerName, peerAvatar, timestamp));

          if (msgType === "audio") {
            const audioBuffer = await this.drive?.get(`/audio/${messageObj.audioName}`);
            /**
             * Triggered when an audio message is received.
             *
             * @event Client#onAudio
             * @property peer - HyperSwarm peer object
             * @property {AudioMessage} AudioMessage - Class with all of the information about received audio file
             * @example
             * ```js
             * const bot = new Client("MyBot");
             * bot.on('onAudio', (peer, message) => {
             *   console.log(`Received audio file from ${message.peerName}`);
             * });
             * ```
             */
            this.emit('onAudio', new AudioMessage(peerName, peerAvatar, this.currentTopic, timestamp, `http://localhost:${this.servePort}/audio/${messageObj.audioName}`, messageObj.audioType, messageObj.audioData));
          }
        }
      });

      peer.on('error', (err: any) => {
        this.emit('onError', err);
        console.error(`Connection error: ${err}`);
      });
    });

    // @ts-ignore
    this.swarm.on("update", () => {
      console.log(`Connections count: ${this.swarm?.connections.size} / Peers count: ${this.swarm?.peers.size}`);
    });
  }
  /**
   * @description Joins a specified chat room.
   * @since 1.0
   * @author snxraven
   * @param chatRoomID Chat room topic string
   */
  joinChatRoom(chatRoomID: string) {
    if (!chatRoomID) {
      console.error("Invalid chat room ID!");
      return;
    }

    this.joinedRooms?.add(chatRoomID); // Add the room to the list of joined rooms
    this.currentTopic = chatRoomID; // Store the current topic
    this.discovery = this.swarm?.join(Buffer.from(chatRoomID, 'hex'), { client: true, server: true });
    this.discovery?.flushed().then(() => {
      console.log(`Bot ${this.botName} joined the chat room.`);
      this.emit('onBotJoinRoom', chatRoomID);
    });
  }

  /**
   * @description Sends a text message.
   * @since 1.0
   * @author MiTask
   * @param message Text message to send to the bot's current chat room.
   */
  sendTextMessage(message: string) {
    console.log(`Preparing to send text message: ${message}`);
    this.sendMessage(TextMessage.new(this, message));
  }

  /**
   * @description Sends a file message.
   * @since 1.0
   * @author snxraven
   * @param filePath Path to the file to send.
   * @param fileType Type of the file to send.
   */
  async sendFileMessage(filePath: string, fileType: string) {
    try {
      await this.drive?.ready();
      const fileBuffer = fs.readFileSync(filePath);
      const fileName = path.basename(filePath);
      await this.drive?.put(`/files/${fileName}`, fileBuffer);
      const fileUrl = `http://localhost:${this.servePort}/files/${fileName}`;
      const fileMessage = FileMessage.new(this, fileName, fileUrl, fileType, fileBuffer); // Pass fileBuffer to the new method
      this.sendMessage(fileMessage);
    } catch (error) {
      console.error('Error sending file message:', error);
    }
  }

  /**
   * @description Sends an audio message.
   * @since 1.0
   * @author snxraven
   * @param filePath Path to the audio file to send.
   * @param audioType Type of the audio file to send.
   */
  async sendAudioMessage(filePath: string, audioType: string) {
    try {
      await this.drive?.ready();
      const audioBuffer = fs.readFileSync(filePath);
      const audioName = path.basename(filePath);
      await this.drive?.put(`/audio/${audioName}`, audioBuffer);
      const audioUrl = `http://localhost:${this.servePort}/audio/${audioName}`;
      const audioMessage = AudioMessage.new(this, audioUrl, audioType, audioBuffer); // Pass audioBuffer to the new method
      this.sendMessage(audioMessage);
    } catch (error) {
      console.error('Error sending audio message:', error);
    }
  }


  /**
   * @description Sends a generic message.
   * @since 1.0
   * @author MiTask
   * @param message Message class (TextMessage, FileMessage or AudioMessage)
   */
  sendMessage(message: Message) {
    console.log("Sending message:", message);
    const data = message.toJsonString();
    const peers = [...this.swarm?.connections];
    if (peers.length === 0) {
      console.warn("No active peer connections found.");
      return;
    }

    console.log(`Sending message to ${peers.length} peers.`);
    for (const peer of peers) {
      try {
        peer.write(data);
        console.log(`Message sent to peer: ${peer.remoteAddress}`);
      } catch (error) {
        console.error(`Failed to send message to peer: ${peer.remoteAddress}`, error);
      }
    }
  }

  /**
   * @description Disconnects the bot and shuts down the Hyperswarm network.
   * @since 1.0
   * @author snxraven
   */
  async destroy() {
    await this.swarm?.destroy();
    console.log(`Bot ${this.botName} disconnected.`);
  }

  /**
   * @description Adds command to the bot Commands array
   * @param command Command to register
   * @since 1.2
   * @author MiTask
   */
  registerCommand(command: Command) {
    console.log(`Registering command "${command.options.name}" with aliases: [${command.options.aliases?.join(", ")}]`)
    this.commands.push(command)
  }

  /**
   * @description Removes command from the bot Commands array
   * @param command Command to unregister
   * @since 1.2
   * @author MiTask
   */
  unregisterCommand(command: Command) {
    console.log(`Unregistering command "${command.options.name}"`)
    this.commands = this.commands.filter(cmd => cmd.options.name !== command.options.name)
  }

  /**
   * @description Registers all classes that extend Command class on specified path
   * @param path Path to search for commands (Must be full path. For example using __dirname)
   * @since 1.2
   * @author MiTask
   */
  public async registerCommands(path: String) {
    const commands = await promisify(glob)(normalize(path + "/**/*.{ts,js}"));
    for (const commandPath of commands) {
      try {
        let command: MaybeCommand = await import(commandPath);
        if ('default' in command) command = command.default;
        if (command.constructor.name === 'Object') command = Object.values(command)[0];

        const instance = new (command as Constructor<Command>)();
        if (!instance.options || !instance.options.name) {
          console.log(`Invalid command class (Missing options or options.name) at ${commandPath}`)
          continue;
        }

        this.registerCommand(instance)
      } catch (e) {
        if(e instanceof TypeError) {
          console.warn(`Invalid command class at ${commandPath}`)
          continue;
        }

        const error = (e instanceof Error) ? e.message : String(e)
        console.log(`Error during loading the command ${commandPath}:\n${error}`)
      }
    }
  }
}

export type Constructor<T extends {} = {}> = new (...args: any[]) => T;
export type MaybeCommand = Constructor<Command> | {default: Constructor<Command>} | {[k: string]: Constructor<Command>};