#
# Cookbook Name:: postgresql_lwrp
# Resource:: default
#
# Author:: Kirill Kouznetsov (agon.smith@gmail.com)
#
# Copyright (C) 2014 LLC Express 42
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
# of the Software, and to permit persons to whom the Software is furnished to do
# so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#

include Chef::Postgresql::Helpers

provides :postgresql
resource_name :postgresql

default_action :create

property :cluster_name, String, required: true
property :cluster_version, String, regex: [/\A(1\d|9\.[1-6])\Z\z/]
property :cookbook, String, default: 'postgresql_lwrp'
property :cluster_create_options, Hash, default: {}
property :configuration, Hash, default: {}
property :hba_configuration, Array, default: []
property :ident_configuration, Array, default: []
property :replication, Hash, default: {}
property :replication_initial_copy, [TrueClass, FalseClass], default: false
property :replication_start_slave, [TrueClass, FalseClass], default: false
property :allow_restart_cluster, default: :none, callbacks: {
  'is not allowed! Allowed params for allow_restart_cluster: first, always or none' => proc do |value|
    !value.to_sym.match(/^(first|always|none)$/).nil?
  end,
}

action :create do
  configuration            = Chef::Mixin::DeepMerge.merge(node['postgresql']['defaults']['server']['configuration'].to_hash, new_resource.configuration)
  hba_configuration        = node['postgresql']['defaults']['server']['hba_configuration'] | new_resource.hba_configuration
  ident_configuration      = node['postgresql']['defaults']['server']['ident_configuration'] | new_resource.ident_configuration

  cluster_name             = new_resource.name
  cluster_version          = (!new_resource.cluster_version.empty? && new_resource.cluster_version) || node['postgresql']['defaults']['server']['version']
  service_name             = "postgresql_#{cluster_version}_#{cluster_name}"

  allow_restart_cluster    = new_resource.allow_restart_cluster

  replication              = Chef::Mixin::DeepMerge.merge(node['postgresql']['defaults']['server']['replication'].to_hash, new_resource.replication)
  replication_file         = "/var/lib/postgresql/#{cluster_version}/#{cluster_name}/recovery.conf"
  replication_start_slave  = new_resource.replication_start_slave
  replication_initial_copy = new_resource.replication_initial_copy

  cluster_options          = Mash.new(new_resource.cluster_create_options)
  parsed_cluster_options   = []

  first_time               = pg_installed?("postgresql-#{cluster_version}")

  %w(locale lc-collate lc-ctype lc-messages lc-monetary lc-numeric lc-time).each do |option|
    parsed_cluster_options << "--#{option} #{cluster_options[:locale]}" if cluster_options[option]
  end

  # Locale hack
  if new_resource.cluster_create_options.key?('locale') && !new_resource.cluster_create_options['locale'].empty?
    system_lang = ENV['LANG']
    ENV['LANG'] = new_resource.cluster_create_options['locale']
  end

  # Configuration hacks
  configuration_hacks(configuration, cluster_version)

  # Install postgresql-common package
  package 'postgresql-common'

  file '/etc/postgresql-common/createcluster.conf' do
    content "create_main_cluster = false\n"
    only_if { cluster_version.to_f >= 9.2 }
  end

  # Install postgresql server packages
  %W(postgresql-#{cluster_version} postgresql-contrib-#{cluster_version} postgresql-server-dev-#{cluster_version}).each do |pkg|
    package pkg
  end

  # Install pgxn client to download custom extensions
  package 'pgxnclient'
  package 'build-essential'

  # Return locale
  if new_resource.cluster_create_options.key?('locale') && !new_resource.cluster_create_options['locale'].empty?
    ENV['LANG'] = system_lang
  end

  # Systemd not working with cluster names with dashes
  # see http://comments.gmane.org/gmane.comp.db.postgresql.debian/346
  if systemd_used? && cluster_name.include?('-')
    raise "Sorry, systemd not support cluster names with dashes ('-'), use underscore ('_') instead"
  end

  # Create postgresql cluster directories
  %W(
    /etc/postgresql
    /etc/postgresql/#{cluster_version}
    /etc/postgresql/#{cluster_version}/#{cluster_name}
    /var/lib/postgresql
    /var/lib/postgresql/#{cluster_version}
  ).each do |dir|
    directory dir do
      owner 'postgres'
      group 'postgres'
    end
  end

  directory "/var/lib/postgresql/#{cluster_version}/#{cluster_name}" do
    mode '0700'
    owner 'postgres'
    group 'postgres'
  end

  # Exec pg_cluster create
  execute 'Exec pg_createcluster' do
    command "pg_createcluster #{parsed_cluster_options.join(' ')} #{cluster_version} #{cluster_name}"
    not_if do
      ::File.exist?("/etc/postgresql/#{cluster_version}/#{cluster_name}/postgresql.conf") ||
        !replication.empty?
    end
  end

  # Define postgresql service
  postgresql_service = service service_name do
    action :nothing
    provider Chef::Provider::Service::Simple
    start_command "pg_ctlcluster #{cluster_version} #{cluster_name} start"
    stop_command "pg_ctlcluster #{cluster_version} #{cluster_name} stop"
    reload_command "pg_ctlcluster #{cluster_version} #{cluster_name} reload"
    restart_command "pg_ctlcluster #{cluster_version} #{cluster_name} restart"
    status_command "su -c '/usr/lib/postgresql/#{cluster_version}/bin/pg_ctl \
 -D /var/lib/postgresql/#{cluster_version}/#{cluster_name} status' postgres"
    supports status: true, restart: true, reload: true
  end

  # Ruby block for postgresql smart restart
  ruby_block "restart_service_#{service_name}" do
    action :nothing
    block do
      if !replication.empty? && !replication_start_slave
        run_context.notifies_delayed(Chef::Resource::Notification.new(postgresql_service, :reload, self))
      elsif need_to_restart?(allow_restart_cluster.to_sym, first_time)
        run_context.notifies_delayed(Chef::Resource::Notification.new(postgresql_service, :restart, self))
      else
        run_context.notifies_delayed(Chef::Resource::Notification.new(postgresql_service, :reload, self))
      end
    end
  end

  # Configuration templates

  main_configuration = configuration.dup
  main_configuration.delete('ssl_cert_file') if cluster_version.to_f < 9.2
  main_configuration.delete('ssl_key_file') if cluster_version.to_f < 9.2
  main_configuration.delete('checkpoint_segments') if cluster_version.to_f > 9.4

  # Backu hack:
  # Set `archive_command` option automatically according to the tool of the
  # choise in `postgresql_cloud_backup` resource assosiated with this PG
  # instance if `archive command` config option is set to `:cloud_auto`
  with_run_context :root do
    t = run_context.resource_collection.select do |res|
      res.resource_name == :postgresql_cloud_backup &&
        res.in_cluster == cluster_name &&
        res.in_version == cluster_version
    end

    if !t.empty? &&
       main_configuration['archive_command'] == :cloud_auto
      backup_utility = t.first.utility
      backup_utility_bin = node['postgresql']['cloud_backup'][backup_utility.sub('-', '_')]['bin']
      main_configuration['archive_command'] = "envdir /etc/#{backup_utility}.d/#{cluster_name}-#{cluster_version}/env/ #{backup_utility_bin} wal-push %p"
    end
  end

  template "/etc/postgresql/#{cluster_version}/#{cluster_name}/postgresql.conf" do
    source 'postgresql.conf.erb'
    owner 'postgres'
    group 'postgres'
    mode '0644'
    variables(
      configuration: main_configuration,
      cluster_name: cluster_name,
      cluster_version: cluster_version
    )
    cookbook new_resource.cookbook
    notifies :run, "ruby_block[restart_service_#{service_name}]", :delayed
  end

  template "/etc/postgresql/#{cluster_version}/#{cluster_name}/pg_hba.conf" do
    source 'pg_hba.conf.erb'
    owner 'postgres'
    group 'postgres'
    mode '0644'
    variables configuration: hba_configuration
    cookbook new_resource.cookbook
    notifies :run, "ruby_block[restart_service_#{service_name}]", :delayed
  end

  template "/etc/postgresql/#{cluster_version}/#{cluster_name}/pg_ident.conf" do
    source 'pg_ident.conf.erb'
    owner 'postgres'
    group 'postgres'
    mode '0644'
    variables configuration: ident_configuration
    cookbook new_resource.cookbook
    notifies :run, "ruby_block[restart_service_#{service_name}]", :delayed
  end

  file "/etc/postgresql/#{cluster_version}/#{cluster_name}/start.conf" do
    content "auto\n"
  end

  # Replication
  if !replication.empty?

    if replication_initial_copy
      pg_basebackup_path = "/usr/lib/postgresql/#{cluster_version}/bin/pg_basebackup"
      pg_data_directory = "/var/lib/postgresql/#{cluster_version}/#{cluster_name}"

      BASEBACKUP_PARAMS = {
        'host' => '-h',
        'port' => '-p',
        'user' => '-U',
        'password' => '-W',
      }.freeze

      conninfo_hash = Hash[*replication[:primary_conninfo].scan(/\w+=[^\s]+/).map { |x| x.split('=', 2) }.flatten]

      basebackup_conninfo_hash = conninfo_hash.map do |key, val|
        "#{BASEBACKUP_PARAMS[key.to_s]} #{val}" if BASEBACKUP_PARAMS[key.to_s]
      end.compact

      fetch_wal_logs = if cluster_version.to_i >= 10
                         '-X fetch'
                       else
                         '-x'
                       end

      execute 'Make basebackup' do
        command "#{pg_basebackup_path} -D #{pg_data_directory} -F p #{fetch_wal_logs} -c fast #{basebackup_conninfo_hash.join(' ')}"
        user 'postgres'
        not_if { ::File.exist?("/var/lib/postgresql/#{cluster_version}/#{cluster_name}/base") }
        timeout 604_800
      end
    end

    link "/var/lib/postgresql/#{cluster_version}/#{cluster_name}/server.key" do
      to configuration['ssl_key_file']
      not_if { cluster_version.to_f > 9.1 && ::File.exist?("/var/lib/postgresql/#{cluster_version}/#{cluster_name}/server.key") }
    end

    link "/var/lib/postgresql/#{cluster_version}/#{cluster_name}/server.crt" do
      to configuration['ssl_cert_file']
      not_if { cluster_version.to_f > 9.1 && ::File.exist?("/var/lib/postgresql/#{cluster_version}/#{cluster_name}/server.crt") }
    end

    template "/var/lib/postgresql/#{cluster_version}/#{cluster_name}/recovery.conf" do
      source 'recovery.conf.erb'
      owner 'postgres'
      group 'postgres'
      mode '0644'
      variables replication: replication
      cookbook new_resource.cookbook
      notifies :run, "ruby_block[restart_service_#{service_name}]", :delayed
    end

  else

    file replication_file do
      action :delete
      notifies :run, "ruby_block[restart_service_#{service_name}]", :delayed
    end
  end

  # Start postgresql
  ruby_block "start_service_#{service_name}]" do
    block do
      run_context.notifies_delayed(Chef::Resource::Notification.new(postgresql_service, :start, self))
    end
    not_if { pg_running?(cluster_version, cluster_name) || (!replication.empty? && !replication_start_slave) }
  end
end
