# frozen_string_literal: true

require 'rack'
require 'yaml'

module DiverDown
  class Web
    WEB_DIR = File.expand_path('../../web', __dir__)

    require 'diver_down/web/action'
    require 'diver_down/web/definition_to_dot'
    require 'diver_down/web/definition_enumerator'
    require 'diver_down/web/bit_id'
    require 'diver_down/web/metadata'
    require 'diver_down/web/indented_string_io'
    require 'diver_down/web/definition_store'
    require 'diver_down/web/definition_loader'
    require 'diver_down/web/definition_module_dependencies'
    require 'diver_down/web/source_alias_resolver'
    require 'diver_down/web/definition_filter'

    # For development
    autoload :DevServerMiddleware, 'diver_down/web/dev_server_middleware'

    # @return [DiverDown::Web::DefinitionStore]
    def self.store
      @store ||= DiverDown::Web::DefinitionStore.new
    end

    # @param definition_dir [String]
    # @param metadata [DiverDown::Web::Metadata]
    # @param store [DiverDown::Web::DefinitionStore]
    def initialize(definition_dir:, metadata:)
      @metadata = metadata
      @files_server = Rack::Files.new(File.join(WEB_DIR))

      definition_files = ::Dir["#{definition_dir}/**/*.{yml,yaml,msgpack,json}"].sort
      @total_definition_files_size = definition_files.size
      @action = nil

      load_definition_files_on_thread(definition_files)
    end

    # @param env [Hash]
    # @return [Array[Integer, Hash, Array]]
    def call(env)
      request = Rack::Request.new(env)

      if @action&.store.object_id != self.class.store.object_id
        @action = DiverDown::Web::Action.new(store: self.class.store, metadata: @metadata)
      end

      case [request.request_method, request.path]
      in ['GET', %r{\A/api/definitions\.json\z}]
        @action.definitions(
          page: request.params['page']&.to_i || 1,
          per: request.params['per']&.to_i || 100,
          title: request.params['title'] || '',
          source: request.params['source'] || '',
          definition_group: request.params['definition_group'] || ''
        )
      in ['GET', %r{\A/api/sources\.json\z}]
        @action.sources
      in ['GET', %r{\A/api/modules\.json\z}]
        @action.modules
      in ['GET', %r{\A/api/modules/(?<modulee>[^/]+)\.json\z}]
        modulee = CGI.unescape(Regexp.last_match[:modulee])
        @action.module(modulee)
      in ['GET', %r{\A/api/definitions/(?<bit_id>\d+)\.json\z}]
        bit_id = Regexp.last_match[:bit_id].to_i
        compound = request.params['compound'] == '1'
        concentrate = request.params['concentrate'] == '1'
        only_module = request.params['only_module'] == '1'
        remove_internal_sources = request.params['remove_internal_sources'] == '1'
        focus_modules = request.params['focus_modules'] || []
        modules = request.params['modules'] || []
        @action.combine_definitions(bit_id, compound, concentrate, only_module, remove_internal_sources, focus_modules, modules)
      in ['GET', %r{\A/api/sources/(?<source>[^/]+)\.json\z}]
        source = Regexp.last_match[:source]
        @action.source(source)
      in ['POST', %r{\A/api/sources/(?<source>[^/]+)/module.json\z}]
        source = Regexp.last_match[:source]
        modulee = request.params['module'] || ''
        @action.set_module(source, modulee)
      in ['POST', %r{\A/api/sources/(?<source>[^/]+)/memo.json\z}]
        source = Regexp.last_match[:source]
        memo = request.params['memo'] || ''
        @action.set_memo(source, memo)
      in ['GET', %r{\A/api/pid\.json\z}]
        @action.pid
      in ['GET', %r{\A/api/initialization_status\.json\z}]
        @action.initialization_status(@total_definition_files_size)
      in ['GET', %r{\A/api/source_aliases\.json\z}]
        @action.source_aliases
      in ['POST', %r{\A/api/source_aliases\.json\z}]
        alias_name = request.params['alias_name']
        old_alias_name = request.params['old_alias_name'] # NOTE: nillable
        source_names = request.params['source_names'] || []

        @action.update_source_alias(alias_name, old_alias_name, source_names)
      in ['GET', %r{\A/assets/}]
        @files_server.call(env)
      in ['GET', /\.json\z/], ['POST', /\.json\z/]
        @action.not_found
      else
        @files_server.call(env.merge('PATH_INFO' => '/index.html'))
      end
    end

    private

    def load_definition_files_on_thread(definition_files)
      definition_loader = DiverDown::Web::DefinitionLoader.new
      store = self.class.store

      Thread.new do
        loop do
          break if definition_files.empty?

          # If store is changed in test, stop loading.
          break if store != self.class.store

          definition_file = definition_files.shift
          definition = definition_loader.load_file(definition_file)

          # No needed to synchronize because this is executed on a single thread.
          store.set(definition)
        end

        # Cache combined_definition
        store.combined_definition

        # For development
        if ENV['DIVER_DOWN_DIR']
          File.write(File.expand_path('../../tmp/combined_definition.yml', __dir__), store.combined_definition.to_h.to_yaml)
        end
      end
    end
  end
end