Clone
require 'locomotive/wagon/version'
require 'locomotive/wagon/logger'
require 'locomotive/wagon/exceptions'

module Locomotive
  module Wagon

    # Authenticate an user to the Hosting platform.
    # If the user does not exist, then create an account for her/him.
    # At the end, store the API key in the ~/.netrc file
    #
    # @param [ String ] email The email of the user
    # @param [ String ] password The password of the user
    # @param [ Object ] shell Used to ask for/prompt information
    #
    def self.authenticate(email, password, shell)
      require 'locomotive/wagon/misc/hosting_api'
      require 'netrc'

      api_key = nil
      api     = Locomotive::HostingAPI.new(email: email, password: password)

      if api.authenticated?
        # existing account
        api_key = api.api_key
        shell.say "You have been successfully authenticated.", :green
      else
        # new account?
        shell.say "No account found for #{email} or invalid credentials", :yellow

        if shell.yes?('Do you want to create a new account? [Y/N]')
          name = shell.ask 'What is your name?'

          account = api.create_account(name: name, email: email, password: password)

          if account.success?
            shell.say "Your account has been successfully created.", :green
            api_key = account['api_key']
          else
            shell.say "We were unable to create your account, reason(s): #{account.error_messages.join(', ')}", :red
          end
        end
      end

      if api_key
        # record the credentials
        netrc = Netrc.read
        netrc[api.domain_with_port] = email, api_key
        netrc.save
      else
        shell.say "We were unable to authenticate you on our platform.", :red
      end
    end

    # Create a site from a site generator.
    #
    # @param [ Object ] generator The wrapping class of the generator itself
    # @param [ Array ] args [name of the site, destination path of the site, skip bundle flag, force_haml]
    # @param [ Hash ] options General options (ex: --force-color)
    #
    def self.init(generator_klass, args, options = {})
      args, opts = Thor::Options.split(args)

      generator = generator_klass.new(args, opts, {})
      generator.force_color_if_asked(options)
      generator.invoke_all
    end

    # Start the thin server which serves the LocomotiveCMS site from the system.
    #
    # @param [ String ] path The path of the site
    # @param [ Hash ] options The options for the thin server (host, port)
    #
    def self.serve(path, options)
      if reader = self.require_mounter(path, true)
        Bundler.require 'misc'

        use_listen = !options[:disable_listen]

        if options[:force]
          begin
            self.stop(path)
            sleep(2) # make sure we wait enough for the server process to stop
          rescue
          end
        end

        # TODO: new feature -> pick the right Rack handler (Thin, Puma, ...etc)
        server = self.thin_server(reader, options.slice(:host, :port, :disable_listen, :live_reload_port))

        if options[:daemonize]
          # very important to get the parent pid in order to differenciate the sub process from the parent one
          parent_pid = Process.pid

          # The Daemons gem closes all file descriptors when it daemonizes the process. So any logfiles that were opened before the Daemons block will be closed inside the forked process.
          # So, close the current logger and set it up again when daemonized.
          Locomotive::Wagon::Logger.close

          server.log_file = File.join(File.expand_path(path), 'log', 'server.log')
          server.pid_file = File.join(File.expand_path(path), 'log', 'server.pid')
          server.daemonize

          use_listen = Process.pid != parent_pid && !options[:disable_listen]

          if Process.pid != parent_pid
            # A "new logger" inside the daemon.
            Locomotive::Wagon::Logger.setup(path, false)
          end
        end

        # listen_thread = Thread.new do
        Locomotive::Wagon::Listen.instance.start(reader) if use_listen

        server.start
        # end

        # server_thread = Thread.new { server.start }

        # hit Control + C to stop
        # Signal.trap('INT')  { EventMachine.stop }
        # Signal.trap('TERM') { EventMachine.stop }

        # listen_thread.join
        # server_thread.join
      end
    end

    def self.stop(path)
      pid_file = File.join(File.expand_path(path), 'log', 'server.pid')
      pid = File.read(pid_file).to_i
      Process.kill('TERM', pid)
    end

    # Generate components for the LocomotiveCMS site such as content types, snippets, pages.
    #
    # @param [ Symbol ] name The name of the generator
    # @param [ Array ] *args The arguments for the generator
    # @param [ Hash ] options The options for the generator
    #
    def self.generate(name, args, options = {})
      Bundler.require 'misc'

      lib = "locomotive/wagon/generators/#{name}"
      require lib

      generator = lib.camelize.constantize.new(args, options, { behavior: :skip })
      generator.force_color_if_asked(options)
      generator.invoke_all
    end

    # Push a site to a remote LocomotiveCMS engine described
    # by the config/deploy.yml file of the site and for a specific environment.
    #
    # @param [ String ] path The path of the site
    # @param [ Hash ] connection_info The information to get connected to the remote site
    # @param [ Hash ] options The options passed to the push process
    #
    def self.push(path, connection_info, options = {})
      if reader = self.require_mounter(path, true)

        reader.mounting_point.site.domains   = connection_info['domains']   if connection_info['domains']
        reader.mounting_point.site.subdomain = connection_info['subdomain'] if connection_info['subdomain']
        require 'bundler'
        Bundler.require 'misc'

        writer    = Locomotive::Mounter::Writer::Api.instance
        resources = self.validate_resources(options[:resources], writer.writers)

        connection_info[:uri] = "#{connection_info.delete(:host)}/locomotive/api"

        _options = { mounting_point: reader.mounting_point, only: resources, console: true }.merge(options).symbolize_keys

        writer.run!(_options.merge(connection_info).with_indifferent_access)
      end
    end

    # Pull a site from a remote LocomotiveCMS engine described
    # by the config/deploy.yml file of the site and for a specific environment.
    #
    # @param [ String ] path The path of the site
    # @param [ Hash ] connection_info The information to get connected to the remote site
    # @param [ Hash ] options The options passed to the pull process
    #
    def self.pull(path, connection_info, options = {})
      self.require_mounter(path)

      Bundler.require 'misc' unless options[:disable_misc]

      connection_info[:uri] = "#{connection_info.delete(:host)}/locomotive/api"

      _options = { console: true }.merge(options).symbolize_keys
      _options[:only] = _options.delete(:resources)

      reader = Locomotive::Mounter::Reader::Api.instance
      self.validate_resources(_options[:only], reader.readers)
      reader.run!(_options.merge(connection_info))

      writer = Locomotive::Mounter::Writer::FileSystem.instance
      writer.run!(_options.merge(mounting_point: reader.mounting_point, target_path: path))
    end

    # Clone a site from a remote LocomotiveCMS engine.
    #
    # @param [ String ] name Name of the site (arbitrary)
    # @param [ String ] path The root path where the site will be cloned
    # @param [ Hash ] connection_info The host, email and password needed to access the remote engine
    # @param [ Hash ] options The options for the API reader
    #
    def self.clone(name, path, connection_info, options = {})
      target_path = File.expand_path(File.join(path, name))

      if File.exists?(target_path)
        puts "Path already exists. If it's an existing site, you might want to pull instead of clone."
        return false
      end

      # generate an almost blank site
      require 'locomotive/wagon/generators/site'
      generator = Locomotive::Wagon::Generators::Site::Cloned
      generator.start [name, path, true, connection_info.symbolize_keys]

      # pull the remote site
      self.pull(target_path, options.merge(connection_info).with_indifferent_access, { disable_misc: true })
    end

    # Destroy a remote site
    #
    # @param [ String ] path The path of the site
    # @param [ Hash ] connection_info The information to get connected to the remote site
    # @param [ Hash ] options The options passed to the push process
    #
    def self.destroy(path, connection_info, options = {})
      self.require_mounter(path)

      connection_info['uri'] = "#{connection_info.delete('host')}/locomotive/api"

      Locomotive::Mounter::EngineApi.set_token connection_info.symbolize_keys
      Locomotive::Mounter::EngineApi.delete('/current_site.json')
    end

    # Load the Locomotive::Mounter lib and set it up (logger, ...etc).
    # If the second parameter is set to true, then the method builds
    # an instance of the reader from the path passed in first parameter.
    #
    # @param [ String ] path The path to the local site
    # @param [ Boolean ] get_reader Tell if it builds an instance of the reader.
    #
    # @param [ Object ] An instance of the reader is the get_reader parameter has been set.
    #
    def self.require_mounter(path, get_reader = false)
      Locomotive::Wagon::Logger.setup(path, false)

      require 'locomotive/mounter'

      Locomotive::Mounter.logger = Locomotive::Wagon::Logger.instance.logger

      if get_reader
        begin
          reader = Locomotive::Mounter::Reader::FileSystem.instance
          reader.run!(path: path)
          reader
        rescue Exception => e
          raise Locomotive::Wagon::MounterException.new "Unable to read the local LocomotiveCMS site. Please check the logs.", e
        end
      end
    end

    protected

    def self.thin_server(reader, options)
      require 'locomotive/wagon/server'
      app = Locomotive::Wagon::Server.new(reader, options)

      # TODO: new feature -> pick the right Rack handler (Thin, Puma, ...etc)
      require 'thin'
      Thin::Server.new(options[:host], options[:port], { signals: true }, app).tap do |server|
        server.threaded = true
      end
    end

    def self.validate_resources(resources, writers_or_readers)
      return if resources.nil?

      # FIXME: for an unknown reason, when called from Cocoa, the resources are a string
      resources = resources.map { |resource| resource.split(' ') }.flatten

      valid_resources = writers_or_readers.map { |thing| thing.to_s.demodulize.gsub(/Writer$|Reader$/, '').underscore }

      resources.each do |resource|
        raise ArgumentError, "'#{resource}' resource not recognized. Valid resources are #{valid_resources.join(', ')}." unless valid_resources.include?(resource)
      end

      resources
    end

  end
end