require 'net/http'
require 'uri'
require 'digest'
require 'sequel'
module Proxy
  module ContainerGateway
    extend ::Proxy::Util
    extend ::Proxy::Log

    class << self
      Sequel.extension :migration, :core_extensions
      def pulp_registry_request(uri)
        http_client = Net::HTTP.new(uri.host, uri.port)
        http_client.ca_file = pulp_ca
        http_client.cert = pulp_cert
        http_client.key = pulp_key
        http_client.use_ssl = true

        http_client.start do |http|
          request = Net::HTTP::Get.new uri
          http.request request
        end
      end

      def ping
        uri = URI.parse("#{Proxy::ContainerGateway::Plugin.settings.pulp_endpoint}/pulpcore_registry/v2/")
        pulp_registry_request(uri).body
      end

      def manifests(repository, tag)
        uri = URI.parse(
          "#{Proxy::ContainerGateway::Plugin.settings.pulp_endpoint}/pulpcore_registry/v2/#{repository}/manifests/#{tag}"
        )
        pulp_registry_request(uri)['location']
      end

      def blobs(repository, digest)
        uri = URI.parse(
          "#{Proxy::ContainerGateway::Plugin.settings.pulp_endpoint}/pulpcore_registry/v2/#{repository}/blobs/#{digest}"
        )
        pulp_registry_request(uri)['location']
      end

      def tags(repository, params = {})
        query = "?"
        unless params[:n].nil? || params[:n] == ""
          query = "#{query}n=#{params[:n]}"
          query = "#{query}&" unless params[:last].nil?
        end
        query = "#{query}last=#{params[:last]}" unless params[:last].nil? || params[:last] == ""

        uri = URI.parse(
          "#{Proxy::ContainerGateway::Plugin.settings.pulp_endpoint}/pulpcore_registry/v2/#{repository}/tags/list#{query}"
        )
        pulp_registry_request(uri)
      end

      def v1_search(params = {})
        if params[:n].nil? || params[:n] == ""
          params[:n] = 25
        else
          params[:n] = params[:n].to_i
        end

        repo_count = 0
        repositories = []
        user = params[:user].nil? ? nil : User.find(name: params[:user])
        Proxy::ContainerGateway.catalog(user).each do |repo_name|
          break if repo_count >= params[:n]

          if params[:q].nil? || params[:q] == "" || repo_name.include?(params[:q])
            repo_count += 1
            repositories << { name: repo_name }
          end
        end
        repositories
      end

      def catalog(user = nil)
        if user.nil?
          unauthenticated_repos
        else
          (unauthenticated_repos + user.repositories_dataset.map(:name)).sort
        end
      end

      def unauthenticated_repos
        Repository.where(auth_required: false).order(:name).map(:name)
      end

      # Replaces the entire list of repositories
      def update_repository_list(repo_list)
        RepositoryUser.dataset.delete
        Repository.dataset.delete
        repo_list.each do |repo|
          Repository.find_or_create(name: repo['repository'],
                                    auth_required: repo['auth_required'].to_s.downcase == "true")
        end
      end

      # Replaces the entire user-repo mapping for all logged-in users
      def update_user_repo_mapping(user_repo_maps)
        # Get hash map of all users and their repositories
        # Ex: {"users"=> [{"admin"=>[{"repository"=>"repo", "auth_required"=>"true"}]}]}
        # Go through list of repositories and add them to the DB
        RepositoryUser.dataset.delete
        user_repo_maps['users'].each do |user_repo_map|
          user_repo_map.each do |user, repos|
            next if repos.nil?

            repos.each do |repo|
              found_repo = Repository.find(name: repo['repository'],
                                           auth_required: repo['auth_required'].to_s.downcase == "true")
              if found_repo.nil?
                logger.warn("#{repo['repository']} does not exist in this smart proxy's environments")
              elsif found_repo.auth_required
                found_repo.add_user(User.find(name: user))
              end
            end
          end
        end
      end

      # Replaces the user-repo mapping for a single user
      def update_user_repositories(username, repositories)
        user = User.where(name: username).first
        user.remove_all_repositories
        repositories.each do |repo_name|
          found_repo = Repository.find(name: repo_name)
          if found_repo.nil?
            logger.warn("#{repo_name} does not exist in this smart proxy's environments")
          elsif user.repositories_dataset.where(name: repo_name).first.nil? && found_repo.auth_required
            user.add_repository(found_repo)
          end
        end
      end

      def authorized_for_repo?(repo_name, user_token_is_valid, username = nil)
        repository = Repository.where(name: repo_name).first

        # Repository doesn't exist
        return false if repository.nil?

        # Repository doesn't require auth
        return true unless repository.auth_required

        if username && user_token_is_valid && repository.auth_required
          # User is logged in and has access to the repository
          user = User.find(name: username)
          return !user.repositories_dataset.where(name: repo_name).first.nil?
        end

        false
      end

      def token_user(token)
        User[AuthenticationToken.find(token_checksum: Digest::SHA256.hexdigest(token)).user_id]
      end

      def valid_token?(token)
        AuthenticationToken.where(token_checksum: Digest::SHA256.hexdigest(token)).where do
          expire_at > Sequel::CURRENT_TIMESTAMP
        end.count.positive?
      end

      def insert_token(username, token, expire_at_string, clear_expired_tokens: true)
        checksum = Digest::SHA256.hexdigest(token)
        user = User.find_or_create(name: username)

        AuthenticationToken.where(:token_checksum => checksum).delete
        AuthenticationToken.create(token_checksum: checksum, expire_at: expire_at_string.to_s, user_id: user.id)
        AuthenticationToken.where { expire_at < Sequel::CURRENT_TIMESTAMP }.delete if clear_expired_tokens
      end

      def initialize_db
        file_path = Proxy::ContainerGateway::Plugin.settings.sqlite_db_path
        conn = Sequel.connect("sqlite://#{file_path}")
        container_gateway_path = $LOAD_PATH.detect { |path| path.include? 'smart_proxy_container_gateway' }
        begin
          Sequel::Migrator.check_current(conn, "#{container_gateway_path}/smart_proxy_container_gateway/sequel_migrations")
        rescue Sequel::Migrator::NotCurrentError
          migrate_db(conn, container_gateway_path)
        end
        conn
      end

      private

      def migrate_db(db_connection, container_gateway_path)
        Sequel::Migrator.run(db_connection, "#{container_gateway_path}/smart_proxy_container_gateway/sequel_migrations")
      end

      def pulp_ca
        Proxy::ContainerGateway::Plugin.settings.pulp_client_ssl_ca
      end

      def pulp_cert
        OpenSSL::X509::Certificate.new(File.read(Proxy::ContainerGateway::Plugin.settings.pulp_client_ssl_cert))
      end

      def pulp_key
        OpenSSL::PKey::RSA.new(
          File.read(Proxy::ContainerGateway::Plugin.settings.pulp_client_ssl_key)
        )
      end
    end

    class Repository < ::Sequel::Model(Proxy::ContainerGateway.initialize_db[:repositories])
      many_to_many :users
    end

    class User < ::Sequel::Model(Proxy::ContainerGateway.initialize_db[:users])
      many_to_many :repositories
      one_to_many :authentication_tokens
    end

    class RepositoryUser < ::Sequel::Model(Proxy::ContainerGateway.initialize_db[:repositories_users]); end

    class AuthenticationToken < ::Sequel::Model(Proxy::ContainerGateway.initialize_db[:authentication_tokens])
      many_to_one :users
    end
  end
end