require 'json'
require 'securerandom'

require 'debci/app'
require 'debci/test_handler'
require 'debci/html_helpers'
require 'debci/validators'
require 'omniauth'
require 'omniauth-gitlab'

module Debci
  class SelfService < Debci::App
    include Debci::TestHandler
    include Debci::Validators::APTSource

    def self.enable_developer_login?
      development? || Debci.config.enable_developer_login?
    end

    def enable_developer_login?
      self.class.enable_developer_login?
    end

    use Rack::MethodOverride
    use OmniAuth::Builder do
      if ::Debci::SelfService.enable_developer_login?
        provider :developer,
                 fields: [:name],
                 uid_field: :name,
                 callback_path: '/user/auth/developer/callback'
      end
      provider :gitlab, Debci.config.salsa_client_id, Debci.config.salsa_client_secret,
               redirect_url: "#{Debci.config.url_base}/user/auth/gitlab/callback",
               scope: "read_user",
               client_options: {
                 site: 'https://salsa.debian.org'
               }
    end

    OmniAuth.config.on_failure = proc do |env|
      OmniAuth::FailureEndpoint.new(env).redirect_to_failure
    end

    OmniAuth.config.logger.level = Logger::UNKNOWN

    configure do
      set :suites, Debci.config.suite_list
      set :archs, Debci.config.arch_list
      set :backends, Debci.config.backend_list
      set :autopkgtest_variants, Debci.config.autopkgtest_variants.keys
    end

    before do
      authenticate! unless request.path == '/user/login' || request.path =~ %r{/user/auth/\w*} || request.path == '/user/theme/'
    end

    get '/' do
      redirect("/user/#{@user.username}")
    end

    post '/theme/' do
      theme = params[:theme]
      if theme.blank? || theme == "auto"
        cookies.delete(:theme)
      else
        cookies[:theme] = theme
      end
      redirect back
    end

    get '/login' do
      redirect(params[:original_url] || "/user/#{@user.username}") if logged_in?

      session[:original_url] ||= params[:original_url]
      erb :login, locals: { csrf: request.env['rack.session']['csrf'] }
    end

    get '/auth/gitlab/callback' do
      uid = request.env['omniauth.auth']['uid']
      username = request.env['omniauth.auth']['info']['username']
      login_callback(uid, username)
    end

    if enable_developer_login?
      [:get, :post].each do |m|
        send(m, '/auth/developer/callback') do
          username = request.env['rack.request.form_hash']['name']
          user = Debci::User.find_by(username: username)
          uid = user&.uid || username
          login_callback(uid, username)
        end
      end
    end

    get '/auth/failure' do
      halt(403, erb("<h2>Authentication Failed</h2><h4>Reason: </h4><pre><%= params[:message] %></pre>"))
    end

    get '/logout' do
      session[:user_id] = nil
      redirect '/'
    end

    get '/:user' do
      redirect("/user/#{params[:user]}/jobs") unless @user.username == params[:user]
      erb :self_service
    end

    get '/:user/test' do
      redirect("/user/#{params[:user]}/jobs") unless @user.username == params[:user]
      erb :self_service_test
    end

    post '/:user/test/submit' do
      trigger = params[:trigger] || ''
      package = params[:package] || ''
      suite = params[:suite] || ''
      archs = (params[:arch] || []).reject(&:empty?)
      pin_packages = (params[:pin_packages] || '').split(/\n+|\r+/).reject(&:empty?)
      is_private = params[:is_private] == "true"
      extra_apt_sources = (params[:extra_apt_sources] || []).reject(&:empty?)
      backend = params[:backend]
      autopkgtest = params[:autopkgtest]

      begin
        # validate inputs
        validate_form_submission(package, suite, archs, extra_apt_sources)
        # assemble test request
        test_obj = {
          'trigger' => trigger,
          'package' => package,
          'backend' => backend,
          'autopkgtest' => autopkgtest,
          'is_private' => is_private,
          'extra-apt-sources' => extra_apt_sources
        }
        test_obj['pin-packages'] = []
        pin_packages.each do |pin_package|
          pin_package = pin_package.split(/,\s*/)
          test_obj['pin-packages'].push(pin_package)
        end
        test_request = {
          'arch' => archs,
          'suite' => suite,
          'tests' => [test_obj]
        }
        # user clicks on export to json
        if params[:export]
          content_type :json
          [200, [test_request].to_json]
        else
          # user submits test
          archs.each do |arch|
            request_tests(test_request['tests'], suite, arch, @user)
          end
          @success = true
          [201, erb(:self_service_test)]
        end
      rescue InvalidRequest => error
        @error_msg = error
        halt(400, erb(:self_service_test))
      end
    end

    get '/:user/retry/:run_id' do
      if @user
        run_id = params[:run_id]
        redirect "user/#{@user.username}/retry/#{run_id}" if params[:user] == ":user"
        @original_job = get_job_to_retry(run_id)
        @same_jobs = get_same_pending_jobs(@original_job).count
        erb :retry
      else
        [403, erb(:cant_retry)]
      end
    end

    post '/:user/retry/:run_id' do
      run_id = params[:run_id]
      j = get_job_to_retry(run_id)
      j.retry
      201
    end

    get '/:user/getkey' do
      redirect "/user/#{@user.username}/keys"
    end

    get '/:user/keys' do
      @keys = Debci::Key.where(user: @user)
      erb :keys
    end

    post '/:user/keys' do
      @key = Debci::Key.create!(params.update(user: @user))
      @keys = Debci::Key.where(user: @user)
      erb :keys
    end

    delete '/:user/keys' do
      Debci::Key.where(user: @user).find(params[:id]).destroy!
      redirect "/user/#{@user.username}/keys"
    end

    class InvalidRequest < RuntimeError
    end

    def validate_form_submission(package, suite, archs, extra_apt_sources)
      raise InvalidRequest.new('Please enter a valid package name') unless valid_package_name?(package)
      raise InvalidRequest.new('Please select a suite') if suite == ''
      raise InvalidRequest.new('Please select an architecture') if archs.empty?
      invalid_extra_apt_sources = invalid_extra_apt_sources(extra_apt_sources)
      raise InvalidRequest.new("Please enter valid extra apt sources: Invalid apt sources: #{invalid_extra_apt_sources}") unless invalid_extra_apt_sources.empty?
      raise InvalidRequest.new("Invalid suite: #{suite}") unless Debci.config.suite_list.include?(suite)
      invalid_archs = archs.reject { |a| Debci.config.arch_list.include?(a) }
      raise InvalidRequest.new("Invalid architectures: #{invalid_archs.join(', ')}") unless invalid_archs.empty?
    end

    post '/:user/test/upload' do
      begin
        raise InvalidRequest.new("Please select a JSON file to upload") if params[:tests].nil?
        test_requests = JSON.parse(File.read(params[:tests][:tempfile]))
        errors = validate_batch_test(test_requests)
        raise InvalidRequest.new(errors.join("; ")) unless errors.empty?
        request_batch_tests(test_requests, @user)
      rescue JSON::ParserError => error
        halt(400, "Invalid JSON: #{error}")
      rescue InvalidRequest => error
        @error_msg = error
        halt(400, erb(:self_service_test))
      else
        @success = true
        [201, erb(:self_service_test)]
      end
    end

    get '/:user/jobs/?' do
      arch_filter = params[:arch]
      suite_filter = params[:suite]
      package_filter = params[:package] || ''
      trigger_filter = params[:trigger] || ''
      user = Debci::User.find_by(username: params[:user])
      query = {
        requestor: user
      }
      disable_private_filter = (@user != user)
      is_private_filter = disable_private_filter ? false : params[:is_private]
      query[:is_private] = is_private_filter unless is_private_filter.nil?
      query[:arch] = arch_filter if arch_filter
      query[:suite] = suite_filter if suite_filter
      @history = Debci::Job.where(query).includes([:package])

      unless package_filter.empty?
        pkgs = Debci::Package.where('name LIKE :query', query: package_filter.tr('*', '%')).pluck(:id)
        @history = @history.where(package_id: pkgs)
      end

      unless trigger_filter.empty?
        @history = @history.where(
          'trigger LIKE :query',
          query: "%#{trigger_filter}%",
        )
      end

      @history = @history.order('date DESC')

      # pagination
      results = get_page_params(@history, params[:page], 20)

      refresh!
      erb :self_service_history, locals: { arch_filter: arch_filter, suite_filter: suite_filter, package_filter: package_filter, trigger_filter: trigger_filter,
                                           is_private_filter: is_private_filter, disable_private_filter: disable_private_filter, results: results }
    end

    get '/:user/test/publish' do
      run_ids = params[:run_ids] || []
      @jobs = Debci::Job.where(requestor: @user, run_id: run_ids, is_private: true)
      erb :publish
    end

    post '/:user/test/publish' do
      run_ids = (params[:run_ids] || []).split(',')
      jobs = Debci::Job.where(requestor: @user, run_id: run_ids)
      jobs.update_all(is_private: false)
      @success = true
      [200, erb(:publish)]
    end

    def get_same_pending_jobs(job)
      Debci::Job.pending.where(
        package_id: job.package_id,
        suite: job.suite,
        arch: job.arch,
        requestor: job.requestor,
        trigger: job.trigger,
        is_private: job.is_private
      ).select { |j| Set.new(j.pin_packages) == Set.new(job.pin_packages) }
       .select { |j| Set.new(j.extra_apt_sources) == Set.new(job.extra_apt_sources) }
    end

    def login_callback(uid, username)
      user = Debci::User.find_or_create_by!(uid: uid) do |c|
        c.username = username
      end
      user.update(username: username) if user.username != username
      session[:user_id] = user.id
      original_url = session[:original_url]
      session.delete(:original_url)
      redirect(original_url || "/user/#{user.username}")
    end
  end
end
