# 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/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 Thor::Shell
      include DbInit
      include Redirection

      Client = Whatup::Server::Client

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

      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
        say "Starting a server with PID:#{@pid} @ #{@address} ... \n", :green

        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|
            case handle_client client
            when :exit
              client.puts 'bye!'
              Thread.kill Thread.current
            end
          end
        end
      rescue SignalException # In case of ^c
        kill
      end

      def find_client_by name:
        @clients.select { |c| c.name == name }&.first
      end

      def clients_except client
        @clients.reject { |c| c == client }
      end

      def new_room! clients: [], name:
        room = Room.create! name: name, clients: clients
        @rooms << room
        room
      end

      private

      # Receives a new client, then continuously gets input from that client
      #
      # rubocop:disable Metrics/MethodLength
      def handle_client client
        client = create_new_client_if_not_existing! client

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

          Thread.current.exit if client.deleted

          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)

          puts "#{client.name}> #{msg}"

          begin
            # Send the output to the client
            redirect stdin: client.socket, stdout: client.socket do
              # Invoke the cli using the provided commands and options.
              run_thor_command! client: client, msg: msg
            end
          rescue RuntimeError,
                 Thor::InvocationError,
                 Thor::UndefinedCommandError => e
            puts e.message
            client.puts 'Invalid input or unknown command'
          rescue ArgumentError => e
            puts e.message
            client.puts e.message
          end
          msg = nil
        end
      end
      # rubocop:enable Metrics/MethodLength

      def handle_dm client
        msg = StringIO.new
        loop do
          input = client.input!
          puts "#{client.name}> #{input}"
          if input == '.exit'
            client.puts "Finished dm to `#{client.composing_dm.name}`."
            break
          end
          msg.puts input
        end
        client.composing_dm
              .received_messages << Message.new(
                sender: client,
                content: msg.string
              )
        client.composing_dm = nil
      end

      def handle_chatting client
        loop do
          input = client.input!
          room = client.room
          puts "#{client.name}> #{input}"
          if input == '.exit'
            client.leave_room!
            client.puts "Exited `#{room.name}`."
            break
          end
          room.broadcast except: client do
            "#{client.name}> #{input}"
          end
        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.
      def create_new_client_if_not_existing! client
        name     = client.gets.chomp
        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
          Thread.current.exit
        end

        @clients << client = Client.create!(
          name: name,
          socket: client
        )

        puts "#{client.name} just showed up!"
        client.puts <<~MSG
          Hello, #{client.name}!

          Welcome to whatup.

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

      def run_thor_command! client:, msg:
        # 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.
        cmds, opts = Whatup::CLI::Interactive.parse_input msg
        Whatup::CLI::Interactive.new(cmds, opts).tap do |c|
          c.server = self
          c.current_user = client

          # This _should_ achieve the same effect as
          # `Whatup::CLI::Interactive.start(args)`, but allows us to set
          # instance variables on the cli class.
          c.invoke c.args.first, c.args.drop(1)
        end
      end

      def exit_if_pid_exists!
        return unless running?

        say <<~EXIT, :cyan
          A server appears to already be running!
          Check `#{@pid_file}`.
        EXIT

        kill
      end

      def connect_to_socket!
        @socket = TCPServer.open @port
      rescue Errno::EADDRINUSE
        puts 'Address already in use!'
        kill
      end

      def write_pid!
        File.open(@pid_file, 'w') { |f| f.puts Process.pid }
      end

      def running?
        File.file? @pid_file
      end

      def kill
        say "Killing the server with PID:#{Process.pid} ...", :red
        FileUtils.rm_rf @pid_file
        exit
      end
    end
  end
end