# frozen_string_literal: true

require 'bundler'
require 'thor'
require 'sitediff'
require 'sitediff/api'

class SiteDiff
  # SiteDiff CLI.
  class Cli < Thor
    class_option 'directory',
                 type: :string,
                 aliases: '-C',
                 default: 'sitediff',
                 desc: 'Configuration directory'
    class_option :debug,
                 type: :boolean,
                 aliases: '-d',
                 default: false,
                 desc: 'Stop on certain errors and produce error trace backs.'
    class_option 'verbose',
                 type: :boolean,
                 aliases: '-v',
                 default: false,
                 desc: 'Show verbose output in terminal'

    # Command aliases.
    map recrawl: :crawl

    # Thor, by default, exits with 0 no matter what!
    def self.exit_on_failure?
      true
    end

    # Thor, by default, does not raise an error for use of unknown options.
    def self.check_unknown_options?(_config)
      true
    end

    desc 'version', 'Show version information'
    ##
    # Show version information.
    def version
      filename = '../../sitediff.gemspec'
      filename = '/sitediff/sitediff.gemspec' if !File.exist?(File.expand_path(filename, __dir__)) && File.exist?(File.expand_path('/sitediff/sitediff.gemspec', __dir__))

      gemspec = Bundler.load_gemspec(File.expand_path(filename, __dir__))

      output = []
      output.push("Sitediff CLI #{gemspec.version}")
      if options[:verbose]
        output.push("Website: #{gemspec.homepage}")
        output.push("GitHub: #{gemspec.metadata['source_code_uri']}")
      end
      puts output.join("\n")
    end

    option 'paths-file',
           type: :string,
           desc: 'Paths are read (one at a line) from PATHS: ' \
                    'useful for iterating over sanitization rules',
           aliases: '--paths-from-file'
    option 'paths',
           type: :array,
           aliases: '-p',
           desc: 'Specific path or paths to fetch'
    option 'before',
           type: :string,
           desc: 'URL to the "before" site, prefixed to all paths.',
           aliases: '--before-url'
    option 'after',
           type: :string,
           desc: 'URL to the "after" site, prefixed to all paths.',
           aliases: '--after-url'
    option 'report-format',
           type: :string,
           enum: %w[html json],
           default: 'html',
           desc: 'The format in which a report should be generated.'
    option 'before-report',
           type: :string,
           desc: 'URL to use in reports. Useful if port forwarding.',
           aliases: '--before-url-report'
    option 'after-report',
           type: :string,
           desc: 'URL to use in reports. Useful if port forwarding.',
           aliases: '--after-url-report'
    option 'cached',
           type: :string,
           enum: %w[none all before after],
           default: 'before',
           desc: 'Use the cached version of these sites, if available.'
    option 'ignore-whitespace',
           type: :boolean,
           default: false,
           aliases: '-w',
           desc: 'Ignore changes in whitespace.'
    option 'export',
           type: :boolean,
           default: false,
           aliases: '-e',
           desc: 'Export report to files. This option forces HTML format.'
    option 'remove_html_comments',
           type: :boolean,
           default: false,
           aliases: '-c',
           desc: 'Remove HTML comments from the output.'
    desc 'diff [OPTIONS] [CONFIG-FILE]',
         'Compute diffs on configured URLs.'
    ##
    # Computes diffs.
    def diff(config_file = nil)
      # Determine "paths" override based on options.
      if options['paths'] && options['paths-file']
        SiteDiff.log "Can't specify both --paths-file and --paths.", :error
        exit(-1)
      end

      api = Api.new(options['directory'], config_file)
      api_options =
        clean_keys(
          options,
          :paths,
          :paths_file,
          :ignore_whitespace,
          :export,
          :before,
          :after,
          :cached,
          :verbose,
          :debug,
          :report_format,
          :before_report,
          :after_report,
          :remove_html_comments
        )
      api_options[:cli_mode] = true
      puts api_options
      api.diff(api_options)
    end

    option :port,
           type: :numeric,
           default: SiteDiff::Webserver::DEFAULT_PORT,
           desc: 'The port to serve on'
    option :browse,
           type: :boolean,
           default: true,
           desc: 'Whether to open the served content in your browser'
    desc 'serve [OPTIONS] [CONFIG-FILE]',
         'Serve SiteDiff report directory over HTTP.'
    ##
    # Serves SiteDiff report for accessing in the browser.
    def serve(config_file = nil)
      api = Api.new(options['directory'], config_file)
      api_options = clean_keys(options, :browse, :port)
      api.serve(api_options)
    end

    option :depth,
           type: :numeric,
           default: Config::DEFAULT_CONFIG['settings']['depth'],
           desc: 'How deeply to crawl the given site'
    option :crawl,
           type: :boolean,
           default: true,
           desc: 'Run "sitediff crawl" to discover paths.'
    option :preset,
           type: :string,
           enum: Config::Preset.all,
           desc: 'Framework-specific presets to apply.'
    option :concurrency,
           type: :numeric,
           default: Config::DEFAULT_CONFIG['settings']['concurrency'],
           desc: 'Max number of concurrent connections made.'
    option :interval,
           type: :numeric,
           default: Config::DEFAULT_CONFIG['settings']['interval'],
           desc: 'Crawling delay - interval in milliseconds.'
    option :include,
           type: :string,
           default: Config::DEFAULT_CONFIG['settings']['include'],
           desc: 'Optional URL include regex for crawling.'
    option :exclude,
           type: :string,
           default: Config::DEFAULT_CONFIG['settings']['exclude'],
           desc: 'Optional URL exclude regex for crawling.'
    option :curl_options,
           type: :hash,
           default: {},
           desc: 'Options to be passed to curl'
    desc 'init URL [URL]', 'Create a sitediff configuration.'
    ##
    # Initializes a sitediff (yaml) configuration file.
    def init(*urls)
      unless (1..2).cover? urls.size
        SiteDiff.log 'sitediff init requires one or two URLs', :error
        exit(2)
      end
      api_options =
        clean_keys(
          options,
          :depth,
          :concurrency,
          :interval,
          :include,
          :exclude,
          :preset,
          :crawl
        )
        .merge(
          {
            after_url: urls.pop,
            before_url: urls.pop,
            directory: get_dir(options['directory']),
            curl_opts: get_curl_opts(options)
          }
        )

      Api.init(api_options)
    end

    option :url,
           type: :string,
           desc: 'A custom base URL to fetch from'
    desc 'store [CONFIG-FILE]',
         'Cache the current contents of a site for later comparison.'
    ##
    # Caches the current version of the site.
    def store(config_file = nil)
      api = Api.new(options['directory'], config_file)
      api_options = clean_keys(options, :url, :debug)
      api.store(api_options)
    end

    desc 'crawl [CONFIG-FILE]',
         'Crawl the "before" site to discover paths.'
    ##
    # Crawls the "before" site to determine "paths".
    #
    def crawl(config_file = nil)
      api = Api.new(options['directory'], config_file)
      api.crawl
    end

    no_commands do
      # Generates CURL options.
      #
      # TODO: Possibly move to API class.
      def get_curl_opts(options)
        # We do want string keys here
        bool_hash = { 'true' => true, 'false' => false }
        curl_opts = UriWrapper::DEFAULT_CURL_OPTS
                    .clone
                    .merge(options['curl_options'] || {})
                    .merge(options['curl_opts'] || {})
        curl_opts.each { |k, v| curl_opts[k] = bool_hash.fetch(v, v) }
        curl_opts
      end

      ##
      # Ensures that the given directory exists.
      def get_dir(directory)
        # Create the dir. Must go before cache initialization!
        @dir = Pathname.new(directory || '.')
        @dir.mkpath unless @dir.directory?
        @dir.to_s
      end

      ##
      # Clean keys - return a subset of a hash with keys as symbols.
      def clean_keys(hash, *keys)
        new_hash = hash.transform_keys { |k| k.tr('-', '_').to_sym }
        new_hash.slice(*keys)
      end
    end
  end
end
