require "pact_broker/configuration"
require "pact_broker/db"
require "pact_broker/initializers/database_connection"
require "pact_broker/project_root"
require "pact_broker/logging/default_formatter"
require "pact_broker/policies"
require "rack-protection"
require "rack/hal_browser"
require "rack/pact_broker/set_base_url"
require "rack/pact_broker/add_pact_broker_version_header"
require "rack/pact_broker/convert_file_extension_to_accept_header"
require "rack/pact_broker/database_transaction"
require "rack/pact_broker/invalid_uri_protection"
require "rack/pact_broker/ui_request_filter"
require "rack/pact_broker/ui_authentication"
require "rack/pact_broker/configurable_make_it_later"
require "rack/pact_broker/no_auth"
require "rack/pact_broker/reset_thread_data"
require "rack/pact_broker/add_cache_header"
require "rack/pact_broker/add_vary_header"
require "rack/pact_broker/use_when"
require "rack/pact_broker/application_context"
require "sucker_punch"
require "pact_broker/api/middleware/configuration"
require "pact_broker/api/middleware/basic_auth"
require "pact_broker/config/basic_auth_configuration"
require "pact_broker/api/authorization/resource_access_policy"
require "pact_broker/api/middleware/http_debug_logs"
require "pact_broker/application_context"
require "pact_broker/db/advisory_lock"

module PactBroker

  class App
    include PactBroker::Logging
    using Rack::PactBroker::UseWhen

    attr_accessor :configuration, :application_context

    def initialize(application_context = PactBroker::ApplicationContext.default_application_context)
      @application_context = application_context
      @app_builder = ::Rack::Builder.new
      @cascade_apps = []
      @make_it_later_api_auth = ::Rack::PactBroker::ConfigurableMakeItLater.new(Rack::PactBroker::NoAuth)
      @make_it_later_ui_auth = ::Rack::PactBroker::ConfigurableMakeItLater.new(Rack::PactBroker::NoAuth)
      # Can only be required after database connection has been made because the decorators rely on the Sequel models
      @create_pact_broker_api_block = ->() { require "pact_broker/api"; PactBroker.build_api(application_context) }
      @configuration = PactBroker.configuration
      yield configuration if block_given?
      post_configure
      prepare_database
      load_configuration_from_database
      seed_example_data
      print_startup_message
      @configuration.freeze
    end

    # Allows middleware to be inserted at the bottom of the shared middleware stack
    # (ie just before the cascade is called for diagnostic, UI and API).
    # To insert middleware at the top of the stack, initialize
    # the middleware with the app, and run it manually.
    # eg run MyMiddleware.new(app)
    def use *args, &block
      @app_builder.use(*args, &block)
    end

    # private API, not sure if this will continue to be supported
    def use_api_auth middleware
      @make_it_later_api_auth.make_it_later(middleware)
    end

    # private API, not sure if this will continue to be supported
    def use_ui_auth middleware
      @make_it_later_ui_auth.make_it_later(middleware)
    end

    def use_custom_ui custom_ui
      @custom_ui = custom_ui
    end

    def use_custom_api custom_api
      @custom_api = custom_api
    end

    def use_to_create_pact_broker_api &block
      @create_pact_broker_api_block = block
    end

    def call env
      running_app.call env
    end

    private

    attr_reader :custom_ui, :create_pact_broker_api_block

    def post_configure
      SuckerPunch.logger = configuration.custom_logger || SemanticLogger["SuckerPunch"]
      configure_database_connection
      configure_sucker_punch
    end

    def prepare_database
      logger.info "Database schema version is #{PactBroker::DB.version(configuration.database_connection)}"
      lock = PactBroker::DB::AdvisoryLock.new(configuration.database_connection, :migrate, :pg_advisory_lock)
      if configuration.auto_migrate_db
        lock.with_lock do
          ensure_all_database_migrations_are_applied
        end
      else
        logger.info "Skipping database schema migrations as database auto migrate is disabled"
      end

      if configuration.auto_migrate_db_data
        lock.with_lock do
          run_data_migrations
        end
      else
        logger.info "Skipping data migrations"
      end

      require "pact_broker/webhooks/service"
      PactBroker::Webhooks::Service.fail_retrying_triggered_webhooks
    end

    def ensure_all_database_migrations_are_applied
      migration_options = { allow_missing_migration_files: configuration.allow_missing_migration_files }

      if PactBroker::DB.is_current?(configuration.database_connection, migration_options)
        logger.info "Skipping database migrations as the latest migration has already been applied"
      else
        logger.info "Migrating database schema"
        PactBroker::DB.run_migrations(configuration.database_connection, migration_options)
        logger.info "Database schema version is now #{PactBroker::DB.version(configuration.database_connection)}"
      end
    end

    def run_data_migrations
      logger.info "Migrating data"
      PactBroker::DB.run_data_migrations(configuration.database_connection)
    end

    def load_configuration_from_database
      configuration.load_from_database!
    end

    def configure_database_connection
      # Keep this configuration in sync with lib/db.rb
      configuration.database_connection ||= PactBroker.create_database_connection(configuration.database_configuration, configuration.logger)
      PactBroker::DB.connection = configuration.database_connection
      PactBroker::DB.validate_connection_config if configuration.validate_database_connection_config
      PactBroker::DB.set_mysql_strict_mode_if_mysql
      PactBroker::DB.connection.extension(:pagination)
      PactBroker::DB.connection.extension(:statement_timeout)
      PactBroker::DB.connection.extension(:any_not_empty)
      PactBroker::DB.connection.timezone = :utc
      Sequel.datetime_class = DateTime
      Sequel.database_timezone = :utc # Store all dates in UTC, assume any date without a TZ is UTC
      Sequel.application_timezone = :local # Convert dates to localtime when retrieving from database
      Sequel.typecast_timezone = :utc # If no timezone specified on dates going into the database, assume they are UTC
    end

    def seed_example_data
      if configuration.seed_example_data && configuration.example_data_seeder
        logger.info "Seeding example data"
        configuration.example_data_seeder.call
        logger.info "Marking seed as done"
        require "pact_broker/config/repository"
        PactBroker::Config::Repository.new.create_or_update_setting(:seed_example_data, false)
      else
        logger.info "Not seeding example data"
      end
    rescue StandardError => e
      logger.error "Error running example data seeder, #{e.class} #{e.message}", e
    end

    def prepare_app
      configure_middleware

      # need this first so UI login logic is performed before API login logic
      @cascade_apps << build_ui

      if configuration.enable_diagnostic_endpoints
        @cascade_apps << build_diagnostic
      end

      @cascade_apps << build_api
    end

    def configure_middleware
      @app_builder.use PactBroker::Api::Middleware::HttpDebugLogs if configuration.http_debug_logging_enabled
      configure_basic_auth
      configure_rack_protection
      @app_builder.use Rack::PactBroker::ApplicationContext, application_context
      @app_builder.use Rack::PactBroker::InvalidUriProtection
      @app_builder.use Rack::PactBroker::ResetThreadData
      @app_builder.use Rack::PactBroker::AddPactBrokerVersionHeader
      @app_builder.use Rack::PactBroker::AddVaryHeader
      @app_builder.use Rack::Static, :urls => ["/stylesheets", "/css", "/fonts", "/js", "/javascripts", "/images"], :root => PactBroker.project_root.join("public")
      @app_builder.use Rack::Static, :urls => ["/favicon.ico"], :root => PactBroker.project_root.join("public/images"), header_rules: [[:all, {"content-type" => "image/x-icon"}]]
      @app_builder.use Rack::PactBroker::AddCacheHeader
      @app_builder.use Rack::PactBroker::ConvertFileExtensionToAcceptHeader
      # Rack::PactBroker::SetBaseUrl needs to be before the Rack::PactBroker::HalBrowserRedirect
      @app_builder.use Rack::PactBroker::SetBaseUrl, configuration.base_urls

      if configuration.use_hal_browser
        logger.info "Mounting HAL browser"
        @app_builder.use Rack::HalBrowser::Redirect
      else
        logger.info "Not mounting HAL browser"
      end
    end

    def configure_basic_auth
      if configuration.basic_auth_enabled
        logger.info "Configuring basic auth"
        logger.warn "No basic auth credentials are configured" unless configuration.basic_auth_credentials_provided?
        logger.info "Public read access is enabled" if configuration.allow_public_read
        policy = PactBroker::Api::Authorization::ResourceAccessPolicy
                  .build(
                    configuration.allow_public_read,
                    configuration.public_heartbeat,
                    configuration.enable_public_badge_access
                  )

        @app_builder.use PactBroker::Api::Middleware::BasicAuth,
          configuration.basic_auth_write_credentials,
          configuration.basic_auth_read_credentials,
          policy
      end
    end

    def configure_rack_protection
      if configuration.use_rack_protection
        rack_protection_options = {
          logger: logger,
          use: configuration.rack_protection_use,
          except: configuration.rack_protection_except
        }.compact

        logger.info("Configuring Rack::Protection", rack_protection_options)
        @app_builder.use Rack::Protection, rack_protection_options

        is_hal_browser = ->(env) { env["PATH_INFO"] == "/hal-browser/browser.html" }
        not_hal_browser = ->(env) { env["PATH_INFO"] != "/hal-browser/browser.html" }

        @app_builder.use_when not_hal_browser,
          Rack::Protection::ContentSecurityPolicy, configuration.content_security_policy
        @app_builder.use_when is_hal_browser,
          Rack::Protection::ContentSecurityPolicy,
          configuration.content_security_policy.merge(configuration.hal_browser_content_security_policy_overrides)
      end
    end

    def build_ui
      logger.info "Mounting UI"
      require "pact_broker/ui"
      ui_apps = [PactBroker::UI::App.new]
      ui_apps.unshift(@custom_ui) if @custom_ui
      builder = ::Rack::Builder.new
      builder.use Rack::PactBroker::UIRequestFilter
      builder.use @make_it_later_ui_auth
      builder.use Rack::PactBroker::UIAuthentication # deprecate?
      builder.run Rack::Cascade.new(ui_apps)
      builder
    end

    def build_api
      logger.info "Mounting PactBroker::API"
      api_apps = [create_pact_broker_api_block.call]
      api_apps.unshift(@custom_api) if @custom_api
      builder = ::Rack::Builder.new
      builder.use @make_it_later_api_auth
      builder.use Rack::PactBroker::DatabaseTransaction, configuration.database_connection
      builder.run Rack::Cascade.new(api_apps, [404])
      builder
    end

    def build_diagnostic
      require "pact_broker/diagnostic/app"
      builder = ::Rack::Builder.new
      builder.use @make_it_later_api_auth
      builder.run PactBroker::Diagnostic::App.new
      builder
    end

    def configure_sucker_punch
      SuckerPunch.exception_handler = -> (ex, klass, args) do
        PactBroker.logger.warn("Unhandled Suckerpunch error for #{klass}.perform(#{args.inspect})", ex)
      end
    end

    def running_app
      @running_app ||= begin
        prepare_app
        apps = @cascade_apps
        @app_builder.map "/" do
          run Rack::Cascade.new(apps, [404])
        end
        @app_builder
      end
    end

    def print_startup_message
      configuration.log_configuration if configuration.log_configuration_on_startup
      unless configuration.hide_pactflow_messages
        logger.info "\n\n#{'*' * 80}\n\nWant someone to manage your Pact Broker for you? Check out https://pactflow.io/oss for a hardened, fully supported SaaS version of the Pact Broker with an improved UI + more.\n\n#{'*' * 80}\n"
      end
    end
  end
end
