# frozen_string_literal: true

require "pathname"
require "singleton"

require_relative "dev_server/cdn_fonts"
require_relative "dev_server/certificate_manager"
require_relative "dev_server/errors"
require_relative "dev_server/header_hash"
require_relative "dev_server/hot_reload"
require_relative "dev_server/hot_reload/script_injector"
require_relative "dev_server/local_assets"
require_relative "dev_server/proxy_param_builder"
require_relative "dev_server/proxy"
require_relative "dev_server/reload_mode"
require_relative "dev_server/remote_watcher"
require_relative "dev_server/sse"
require_relative "dev_server/watcher"
require_relative "dev_server/web_server"
require_relative "dev_server/hooks/file_change_hook"

require_relative "development_theme"
require_relative "ignore_filter"
require_relative "include_filter"
require_relative "syncer"
require_relative "notifier"

module ShopifyCLI
  module Theme
    class DevServer
      include Singleton

      attr_reader :app, :stopped, :ctx, :root, :host, :theme_identifier, :port, :poll, :editor_sync, :stable, :mode,
        :block, :includes, :ignores, :notify

      class << self
        def start(
          ctx,
          root,
          host: "127.0.0.1",
          theme: nil,
          port: 9292,
          poll: false,
          editor_sync: false,
          overwrite_json: false,
          stable: false,
          mode: ReloadMode.default,
          includes: nil,
          ignores: nil,
          notify: nil,
          &block
        )
          instance.setup(
            ctx,
            root,
            host,
            theme,
            port,
            poll,
            editor_sync,
            overwrite_json,
            stable,
            mode,
            includes,
            ignores,
            notify,
            &block
          )
          instance.start
        end

        def stop
          instance.stop
        end
      end

      # rubocop:disable Metrics/ParameterLists
      def setup(
        ctx,
        root,
        host,
        theme_identifier,
        port,
        poll,
        editor_sync,
        overwrite_json,
        stable,
        mode,
        includes,
        ignores,
        notify,
        &block
      )
        @ctx = ctx
        @root = root
        @host = host
        @theme_identifier = theme_identifier
        @port = port
        @poll = poll
        @editor_sync = editor_sync
        @overwrite_json = overwrite_json
        @stable = stable
        @mode = mode
        @includes = includes
        @ignores = ignores
        @notify = notify
        @block = block
      end

      def start
        sync_theme

        # Handle process stop
        trap("INT") { stop("SIGINT") }
        trap("TERM") { stop("SIGTERM") }

        # Setup the middleware stack. Mimics Rack::Builder / config.ru, but in reverse order
        @app = middleware_stack

        # Start development server
        setup_server
        start_server
        teardown_server

      rescue ShopifyCLI::API::APIRequestForbiddenError,
             ShopifyCLI::API::APIRequestUnauthorizedError
        ctx.abort(ensure_user_message)
      rescue Errno::EADDRINUSE
        ctx.abort(port_error_message, port_error_help_message)
      rescue Errno::EADDRNOTAVAIL
        ctx.abort(binding_error_message)
      end

      def stop(signal = nil)
        ctx.debug(stop_signal(signal)) unless signal.nil?

        @stopped = true

        ctx.puts(stopping_message)
        app.close
        WebServer.shutdown
      end

      private

      def setup_server
        watcher&.start
        remote_watcher&.start if editor_sync
      end

      def teardown_server
        # Use instance variables, so we don't build components
        # at the teardown phase.
        @remote_watcher&.stop if editor_sync
        @watcher&.stop
        @syncer&.shutdown
      end

      def start_server
        WebServer.run(
          app,
          BindAddress: host,
          Port: port,
          Logger: logger,
          AccessLog: [],
        )
      end

      def middleware_stack
        @app = Proxy.new(ctx, theme, param_builder)
        @app = CdnFonts.new(@app, theme: theme)
        @app = LocalAssets.new(ctx, @app, theme)
        @app = HotReload.new(ctx, @app, broadcast_hooks: broadcast_hooks, watcher: watcher, mode: mode,
          script_injector: script_injector)
      end

      def sync_theme
        ctx.print_task(syncing_theme_message)
        syncer.start_threads

        if block
          block.call(syncer)
        else
          syncer.upload_theme!(delay_low_priority_files: true)
        end

        ctx.open_browser_url!(address)
      end

      def theme
        @theme ||= if theme_identifier
          theme = ShopifyCLI::Theme::Theme.find_by_identifier(ctx, root: root, identifier: theme_identifier)
          theme || ctx.abort(not_found_error_message)
        else
          DevelopmentTheme.find_or_create!(ctx, root: root)
        end
      end

      def syncer
        @syncer ||= Syncer.new(
          ctx,
          theme: theme,
          include_filter: include_filter,
          ignore_filter: ignore_filter,
          overwrite_json: !editor_sync || @overwrite_json,
          stable: stable
        )
      end

      def notifier
        @notifier ||= Notifier.new(ctx, path: notify)
      end

      def watcher
        @watcher ||= Watcher.new(
          ctx,
          theme: theme,
          ignore_filter: ignore_filter,
          syncer: syncer,
          poll: poll
        )
      end

      def remote_watcher
        @remote_watcher ||= RemoteWatcher.to(
          theme: theme,
          syncer: syncer
        )
      end

      def param_builder
        @param_builder ||= ProxyParamBuilder
          .new
          .with_theme(theme)
          .with_syncer(syncer)
      end

      def ignore_filter
        @ignore_filter ||= IgnoreFilter.from_path(root).tap do |filter|
          filter.add_patterns(ignores) if ignores
        end
      end

      def include_filter
        @include_filter ||= ShopifyCLI::Theme::IncludeFilter.new(root, includes)
      end

      def logger
        @logger ||= if ctx.debug?
          WEBrick::Log.new(nil, WEBrick::BasicLog::INFO)
        else
          WEBrick::Log.new(nil, WEBrick::BasicLog::FATAL)
        end
      end

      # Hooks

      def broadcast_hooks
        file_handler = Hooks::FileChangeHook.new(ctx, theme: theme, include_filter: include_filter,
          ignore_filter: ignore_filter, notifier: notifier)
        [file_handler]
      end

      def script_injector
        HotReload::ScriptInjector.new(ctx, theme: theme)
      end

      def address
        @address ||= "http://#{host}:#{port}"
      end

      # Messages

      def ensure_user_message
        shop = ShopifyCLI::AdminAPI.get_shop_or_abort(ctx)
        ctx.message("theme.serve.ensure_user", shop)
      end

      def port_error_message
        ctx.message("theme.serve.address_already_in_use", address)
      end

      def port_error_help_message
        ctx.message("theme.serve.try_port_option")
      end

      def binding_error_message
        ctx.message("theme.serve.binding_error", ShopifyCLI::TOOL_NAME)
      end

      def viewing_theme_message
        ctx.message("theme.serve.viewing_theme")
      end

      def syncing_theme_message
        ctx.message("theme.serve.syncing_theme", theme.id, theme.shop)
      end

      def serving_theme_message
        ctx.message("theme.serve.serving", theme.root)
      end

      def stopping_message
        ctx.message("theme.serve.stopping")
      end

      def stop_signal(signal)
        ctx.message("theme.serve.stop_signal", signal)
      end

      def not_found_error_message
        ctx.message("theme.serve.theme_not_found", theme_identifier)
      end

      def preview_message
        preview_suffix = editor_sync ? "" : ctx.message("theme.serve.download_changes")

        ctx.message(
          "theme.serve.customize_or_preview",
          preview_suffix,
          theme.editor_url,
          theme.preview_url
        )
      end
    end
  end
end
