# frozen_string_literal: true

require 'socket'
require 'fileutils'
require 'securerandom'

require 'sqlite3'
require 'active_record'
require 'active_support/core_ext/object/blank'

require 'whatup/server/db_init'
require 'whatup/server/logger'
require 'whatup/server/redirection'
require 'whatup/server/models/client'
require 'whatup/server/models/message'
require 'whatup/server/models/room'
require 'whatup/cli/commands/interactive/interactive'

module Whatup
  module Server
    class Server # rubocop:disable Metrics/ClassLength
      include DbInit
      include Redirection
      include WhatupLogger

      Client = Whatup::Server::Client

      attr_reader *%i[ip port address clients pid pid_file rooms]

      # @param ip [String] The ip address to run the server on
      # @param port [Integer] The port to run the server on
      #
      # @return [Whatup::Server::Server] The created server
      def initialize ip: 'localhost', port:
        @ip = ip
        @port = port
        @address = "#{@ip}:#{@port}"

        @clients = []
        @rooms = []

        @pid = Process.pid
        @pid_file = "#{Dir.home}/.whatup.pid"

        DbInit.setup_db!
      end

      # Starts the server.
      #
      # The server continuously loops, and handle each new client in a separate
      # thread.
      def start
        log.info { "Starting a server with PID:#{@pid} @ #{@address} ... \n" }

        exit_if_pid_exists!
        connect_to_socket!
        write_pid!

        # Listen for connections, then accept each in a separate thread
        loop do
          Thread.new(@socket.accept) do |client|
            log.info { "Accepted new client: #{client.inspect}" }
            handle_client client
          end
        end
      rescue SignalException # In case of ^c
        kill
      end

      # @param client [Whatup::Server::Client] The client to not retrieve
      #
      # @return [Array<Whatup::Server::Client>] All currently connected clients
      #   except for `client`
      def clients_except client
        @clients.reject { |c| c == client }
      end

      # @param clients [Array<Whatup::Server::Client>] Room's inital clients
      # @param name [String] The room's name
      #
      # @return [Whatup::Server::Room] The created room
      def new_room! clients: [], name:
        Room.create!(name: name, clients: clients).tap do |room|
          @rooms << room
        end
      end

      private

      # Receives a new client, then continuously gets input from that client
      #
      # @param client [Whatup::Server::Client] The client
      #
      def handle_client client # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/LineLength
        client = create_new_client_if_not_existing! client

        # Loop forever to maintain the connection
        loop do
          @clients.reject! &:deleted

          if client.deleted
            log.debug do
              <<~OUT
                Client `#{client.name}` has been deleted.
                #{'Killing'.colorize :red} this thread."
              OUT
            end
            Thread.current.exit
          end

          if client.composing_dm?
            handle_dm client
          elsif client.chatting?
            handle_chatting client
          end

          # Wait until we get a valid command. This takes as long as the client
          # takes.
          msg = client.input! unless Whatup::CLI::Interactive.command?(msg)
          log.info { "#{client.name.colorize :light_blue}> #{msg}" }

          # Send the output to the client
          redirect stdin: client.socket, stdout: client.socket do
            begin
              # Invoke the cli using the provided commands and options.
              run_thor_command! client: client, msg: msg
            rescue RuntimeError,
                   ArgumentError,
                   Thor::InvocationError,
                   Thor::UndefinedCommandError => e
              log.info do
                "#{client.name.colorize :red}> #{e.class}: #{e.message}"
              end
              client.puts case e.class.to_s
                          when 'RuntimeError'
                            'Invalid input or unknown command'
                          else
                            e.message
                          end
            end
          end
          msg = nil
        end
      end

      # Handles inputing direct messages
      #
      # @param client [Whatup::Server::Client] `client` is the sender of
      #   the message, and `client.composing_dm` is the recipient.
      def handle_dm client
        msg = StringIO.new
        loop do
          input = client.input!
          log.info { "#{client.name.colorize :light_blue}> #{input}" }
          msg.puts input
          if input == '.exit'
            client.puts "Finished dm to `#{client.composing_dm.name}`."
            break
          end
        end
        Message.create!(sender: client, content: msg.string).tap do |m|
          client.composing_dm.received_messages << m
          log.debug do
            "Created new message (id = #{m.id}) from `#{client}` to" \
            "`#{client.composing_dm}`"
          end
        end
        client.composing_dm = nil
      end

      # Handles chatting.
      #
      # @param client [Whatup::Server::Client] The client. `client` is assumed
      #   to already belong to a room
      def handle_chatting client
        loop do
          input = client.input!
          audience = @clients.reject { |c| c.id == client.id }
                             .select do |c|
                               client.room.clients.pluck(:id).include? c.id
                             end
          log.info { "#{client.name.colorize :light_blue}> #{input}" }
          if input == '.exit'
            client.puts "Exited `#{client.room.name}`."
            audience.each { |c| c.puts "#{client.name}> LEFT" }
            client.leave_room!
            break
          end
          audience.each { |c| c.puts "#{client.name}> #{input}" }
        end
      end

      # Receives a username from a client, then creates a new client unless a
      # client with that username already exists.
      #
      # If no username is provided (i.e, blank), it assigns a random, anonymous
      # username in the format `ANON-xxx`, where `xxx` is a random number upto
      # 100, left-padded with zeros.
      #
      # @param client [TCPSocket] The client connection
      #
      # @return [Whatup::Server::Client] The created client
      def create_new_client_if_not_existing! client
        log.debug { 'Creating new client' }

        name = client.gets&.chomp

        if name.nil?
          log.debug do
            'New client (currently unknown) has left. ' \
            "#{'Killing'.colorize :red} this thread."
          end
          Thread.current.exit
        end

        rand_num = SecureRandom.random_number(100).to_s.rjust 3, '0'
        name     = name == '' ? "ANON-#{rand_num}" : name

        if @clients.any? { |c| c.name == name }
          client.puts 'That name is taken! Goodbye.'
          client.puts 'END'
          client.close
          log.debug do
            "Existing name `#{name}` entered. " \
            "#{'Killing'.colorize :red} this thread"
          end
          Thread.current.exit
        end

        @clients << client = Client.create!(
          name: name,
          socket: client
        )
        log.info { "Created new client `#{name}` ..." }

        client.tap do |c|
          c.puts <<~MSG
            Hello, #{client.name}!

            Welcome to whatup.

            To get started, type `help`.
          MSG
        end
      end

      # Initialize a new cli class using the initial command and options,
      # and then set any instance variables, since Thor will create a new
      # class instance when it's invoked.
      #
      # This achieve the same effect as
      # `Whatup::CLI::Interactive.start(args)`, but allows us to set
      # instance variables on the cli class.
      #
      # @param client [Whatup::Server::Client]
      def run_thor_command! client:, msg:
        cmds, opts = Whatup::CLI::Interactive.parse_input msg
        cli = Whatup::CLI::Interactive.new(
          cmds,
          opts,
          locals: {server: self, current_user: client} # config
        )
        cli.invoke cli.args.first, cli.args.drop(1)
      end

      # Kills the server if a PID for this app exists
      def exit_if_pid_exists!
        log.debug { "Checking if `#{@pid}` exists ..." }

        return unless running?

        log.info <<~EXIT
          A server appears to already be running!
          Check `#{@pid_file}`.
        EXIT

        kill
      end

      # Connect a new socket for this server to start listening on the specified
      # address and port.
      def connect_to_socket!
        log.info do
          "#{'Opening'.colorize :blue} TCP socket at `#{@ip}:#{@port}`"
        end
        @socket = TCPServer.open @ip, @port
      rescue Errno::EADDRINUSE
        log.error "Address `#{@ip}:#{@port}` is already in use!"
        kill
      end

      # Write this process's PID to the PID file
      def write_pid!
        log.debug { "Writing PID to `#{@pid}`" }
        File.open(@pid_file, 'w') { |f| f.puts Process.pid }
      end

      # @return [Bool] Whether or not a PID for this app exists
      def running?
        File.file? @pid_file
      end

      # Kills the server and removes the PID file
      def kill
        log.info do
          "#{'Killing'.colorize :red} the server with " \
          "PID:#{Process.pid} ..."
        end
        FileUtils.rm_rf @pid_file
        log.debug { "Removed `#{@pid_file}`." }
        exit
      end
    end
  end
end