# frozen_string_literal: true

require 'ostruct'
require 'logger'
require 'yaml'

module Keycard
  # Module for database interactions for Keycard.
  module DB
    # Any error with the database that Keycard itself detects but cannot handle.
    class DatabaseError < StandardError; end

    CONNECTION_ERROR = 'The Keycard database is not initialized. Call initialize! first.'

    ALREADY_CONNECTED = 'Already connected; refusing to connect to another database.'

    MISSING_CONFIG = <<~MSG
      KEYCARD_DATABASE_URL and DATABASE_URL are both missing and a connection
      has not been configured. Cannot connect to the Keycard database.
      See Keycard::DB.connect! for help.
    MSG

    LOAD_ERROR = <<~MSG
      Error loading Keycard database models.
      Verify connection information and that the database is migrated.
    MSG

    SCHEMA_HEADER = "# Keycard Database Version\n"

    class << self
      # Initialize Keycard
      #
      # This connects to the database if it has not already happened and
      # requires all of the Keycard model classes. It is required to do the
      # connection setup first because of the design decision in Sequel that
      # the schema is examined at the time of extending Sequel::Model.
      def initialize!
        connect! unless connected?
        begin
          model_files.each do |file|
            require_relative file
          end
        rescue Sequel::DatabaseError, NoMethodError => e
          raise DatabaseError, LOAD_ERROR + "\n" + e.message
        end
        db
      end

      # Connect to the Keycard database.
      #
      # The default is to use the settings under {.config}, but can be
      # supplied here (and they will be merged into config as a side effect).
      # The keys that will be used from either source are documented here as
      # the options.
      #
      # Only one "mode" will be used; the first of these supplied will take
      # precedence:
      #
      # 1. An already-connected {Sequel::Database} object
      # 2. A connection string
      # 3. A connection options hash
      #
      # While Keycard serves as a singleton, this will raise a DatabaseError
      # if already connected. Check `connected?` if you are unsure.
      #
      # @see {Sequel.connect}
      # @param [Hash] config Optional connection config
      # @option config [String] :url A Sequel database URL
      # @option config [Hash]   :opts A set of connection options
      # @option config [Sequel::Database] :db An already-connected database;
      # @return [Sequel::Database] The initialized database connection
      def connect!(config = {})
        raise DatabaseError, ALREADY_CONNECTED if connected?
        merge_config!(config)
        raise DatabaseError, MISSING_CONFIG if self.config.db.nil? && conn_opts.empty?

        # We splat here because we might give one or two arguments depending
        # on whether we have a string or not; to add our logger regardless.
        @db = self.config.db || Sequel.connect(*conn_opts)
      end

      # Run any pending migrations.
      # This will connect with the current config if not already conencted.
      def migrate!
        connect! unless connected?
        Sequel.extension :migration
        Sequel::Migrator.run(db, File.join(__dir__, '../../db/migrations'), table: schema_table)
      end

      def schema_table
        :keycard_schema
      end

      def schema_file
        'db/keycard.yml'
      end

      def dump_schema!
        connect! unless connected?
        version = db[schema_table].first.to_yaml
        File.write(schema_file, SCHEMA_HEADER + version)
      end

      def load_schema!
        connect! unless connected?
        version = YAML.load_file(schema_file)[:version]
        db[schema_table].delete
        db[schema_table].insert(version: version)
      end

      def model_files
        []
      end

      # Merge url, opts, or db settings from a hash into our config
      def merge_config!(config = {})
        self.config.url  = config[:url]  if config.key?(:url)
        self.config.opts = config[:opts] if config.key?(:opts)
        self.config.db   = config[:db]   if config.key?(:db)
      end

      def conn_opts
        log = { logger: Logger.new('db/keycard.log') }
        url = config.url
        opts = config.opts
        if url
          [url, log]
        elsif opts
          [log.merge(opts)]
        else
          []
        end
      end

      def config
        @config ||= OpenStruct.new(
          url: ENV['KEYCARD_DATABASE_URL'] || ENV['DATABASE_URL']
        )
      end

      def connected?
        !@db.nil?
      end

      # The Keycard database
      # @return [Sequel::Database] The connected database; be sure to call initialize! first.
      def db
        raise DatabaseError, CONNECTION_ERROR unless connected?
        @db
      end

      # Forward the Sequel::Database []-syntax down to db for convenience.
      # Everything else must be called on db directly, but this is nice sugar.
      def [](*args)
        db[*args]
      end
    end
  end
end