require 'debci/app'
require 'debci/user'
require 'debci/package'
require 'debci/job'
require 'debci/html_helpers'

module Debci
  class Admin < Debci::App
    include Debci::HTMLHelpers
    set :views, "#{File.dirname(__FILE__)}/html/templates"

    Model = Struct.new(:name, :icon, :ar_class, :order, :fields, :select_fields, :search_fields, :read_only_fields, :edit_fields, :new, :formatters, :parsers)

    set :models, {}

    def self.model(name, **opts)
      model = Model.new(name)
      opts.each do |k, v|
        model.send("#{k}=", v)
      end
      settings.models[name] = model
    end

    model :users,
          icon: :user,
          ar_class: Debci::User,
          order: :username,
          new: true,
          fields: [:username, :uid, :admin],
          search_fields: [:username],
          read_only_fields: [:username]

    model :packages,
          icon: :gears,
          ar_class: Debci::Package,
          order: :name,
          new: false,
          fields: [:name, :removed, :backend, :storage_limit],
          formatters: {
            storage_limit: ->(i) { number_to_human_size(i) },
          },
          parsers: {
            storage_limit: ->(s) { parse_size(s) },
          },
          select_fields: {
            backend: Debci.config.backend_list,
          },
          search_fields: [:name, :backend],
          read_only_fields: [:name]

    before do
      authenticate!
      forbidden unless @user.admin
    end

    get '/' do
      erb :admin_index, locals: { models: settings.models, current_model: nil }
    end

    get '/:model/' do
      halt 404 unless current_model

      rel = current_model.all.order(order)
      if params[:q]
        q = "%#{current_model.sanitize_sql_like(params[:q])}%"
        cond = search_fields.map { |f| "#{f} LIKE ?" }.join(" OR ")
        values = search_fields.map { |_f| q }
        rel = rel.where(cond, *values)
      end
      results = get_page_params(rel, params[:page], 20) # FIXME: 20 is too low
      erb :admin_index, locals: {
        models: settings.models,
        current_model: current_model,
        results: results,
        fields: fields,
      }
    end

    get '/:model/new/' do
      forbidden unless current_model_info.new
      object = current_model.new
      erb :admin_edit, locals: { object: object, fields: fields, select_fields: select_fields, read_only_fields: [] }
    end

    get '/:model/:id/' do
      halt 404 unless current_model

      erb :admin_edit, locals: { object: current_object, fields: fields, select_fields: select_fields, read_only_fields: read_only_fields }
    end

    post '/:model/new/' do
      forbidden unless current_model_info.new
      object = read_object(current_model.new, params, [])
      object.save!
      redirect "/admin/#{current_model_name}/"
    rescue ActiveRecord::RecordInvalid => e
      erb :admin_edit, locals: { object: object, fields: fields, select_fields: select_fields, read_only_fields: [], validation_error: e }
    end

    post '/:model/:id/' do
      read_object(current_object, params, read_only_fields)
      current_object.save!
      redirect "/admin/#{current_model_name}/"
    rescue ActiveRecord::RecordInvalid => e
      erb :admin_edit, locals: { object: current_object, fields: fields, select_fields: select_fields, read_only_fields: read_only_fields, validation_error: e }
    end

    def read_object(object, params, read_only_fields)
      data = params.reject do |key, _value|
        [:model, :id].include?(key.to_sym) || read_only_fields.include?(key.to_sym)
      end
      data.each_key do |f|
        column = current_model.columns_hash[f]
        data[f] = nil if column.null && data[f].blank?
        data[f] = parse(f.to_sym, data[f]) if data[f]
      end
      object.attributes = data
      object
    end

    def current_model_name
      @current_model_name ||= params[:model]
    end

    def current_model_info
      @current_model_info ||= settings.models[params[:model].to_sym]
    end

    def current_model
      @current_model ||= current_model_info.ar_class
    end

    def order
      current_model_info.order || :id
    end

    def current_object
      @current_object ||= current_model.find(params[:id])
    rescue ActiveRecord::RecordNotFound
      halt 404
    end

    def fields
      @fields ||= current_model_info.fields
    end

    def format(field, value)
      return value unless current_model_info.formatters

      field = field.to_sym
      formatter = current_model_info.formatters[field]
      return value unless formatter

      instance_exec(value, &formatter)
    end

    def parse(field, value)
      return value unless current_model_info.parsers

      parser = current_model_info.parsers[field]
      return value unless parser

      instance_exec(value, &parser)
    end

    def parse_size(str)
      n = str.to_f
      case str
      when /mb$/i
        n *= 1.megabyte
      when /gb$/i
        n *= 1.gigabyte
      end
      n
    end

    def select_fields
      @select_fields ||= current_model_info.select_fields || []
    end

    def read_only_fields
      @read_only_fields ||=
        if current_model_info.read_only_fields
          current_model_info.read_only_fields
        elsif current_model_info.edit_fields
          current_model_info.fields - current_model_info.edit_fields
        else
          []
        end
    end

    def search_fields
      @search_fields ||= current_model_info.search_fields
    end
  end
end
