####################################################################################################################################
# Configuration Definition Data
#
# The configuration is defined in src/build/config/config.yaml, which also contains the documentation.
####################################################################################################################################
package pgBackRestDoc::Custom::DocConfigData;

use strict;
use warnings FATAL => qw(all);
use Carp qw(confess);

use Cwd qw(abs_path);
use Exporter qw(import);
    our @EXPORT = qw();
use File::Basename qw(dirname basename);
use Getopt::Long qw(GetOptions);
use Storable qw(dclone);

use pgBackRestDoc::Common::Exception;
use pgBackRestDoc::Common::Log;
use pgBackRestDoc::ProjectInfo;

use pgBackRestTest::Common::Wait;

####################################################################################################################################
# Command constants
####################################################################################################################################
use constant CFGCMD_BACKUP                                          => 'backup';
    push @EXPORT, qw(CFGCMD_BACKUP);
use constant CFGCMD_HELP                                            => 'help';
    push @EXPORT, qw(CFGCMD_HELP);
use constant CFGCMD_INFO                                            => 'info';
    push @EXPORT, qw(CFGCMD_INFO);
use constant CFGCMD_VERSION                                         => 'version';

####################################################################################################################################
# Command role constants - roles allowed for each command. Commands may have multiple processes that work together to implement
# their functionality. These roles allow each process to know what it is supposed to do.
####################################################################################################################################
# Called directly by the user. This is the main process of the command that may or may not spawn other command roles.
use constant CFGCMD_ROLE_MAIN                                       => 'main';
    push @EXPORT, qw(CFGCMD_ROLE_MAIN);

# Async worker that is spawned so the main process can return a result while work continues. An async worker may spawn local or
# remote workers.
use constant CFGCMD_ROLE_ASYNC                                      => 'async';
    push @EXPORT, qw(CFGCMD_ROLE_ASYNC);

# Local worker for parallelizing jobs. A local work may spawn a remote worker.
use constant CFGCMD_ROLE_LOCAL                                      => 'local';
    push @EXPORT, qw(CFGCMD_ROLE_LOCAL);

# Remote worker for accessing resources on another host
use constant CFGCMD_ROLE_REMOTE                                     => 'remote';
    push @EXPORT, qw(CFGCMD_ROLE_REMOTE);

####################################################################################################################################
# Option constants - options that are allowed for commands
####################################################################################################################################

# Command-line only options
#-----------------------------------------------------------------------------------------------------------------------------------
use constant CFGOPT_CONFIG                                          => 'config';
    push @EXPORT, qw(CFGOPT_CONFIG);
use constant CFGOPT_STANZA                                          => 'stanza';
    push @EXPORT, qw(CFGOPT_STANZA);

# Command-line only local/remote options
#-----------------------------------------------------------------------------------------------------------------------------------
# Paths
use constant CFGOPT_LOCK_PATH                                       => 'lock-path';
    push @EXPORT, qw(CFGOPT_LOCK_PATH);
use constant CFGOPT_LOG_PATH                                        => 'log-path';
    push @EXPORT, qw(CFGOPT_LOG_PATH);
use constant CFGOPT_SPOOL_PATH                                      => 'spool-path';
    push @EXPORT, qw(CFGOPT_SPOOL_PATH);

# Logging
use constant CFGOPT_LOG_LEVEL_STDERR                                => 'log-level-stderr';
    push @EXPORT, qw(CFGOPT_LOG_LEVEL_STDERR);
use constant CFGOPT_LOG_TIMESTAMP                                   => 'log-timestamp';
    push @EXPORT, qw(CFGOPT_LOG_TIMESTAMP);

# Repository options
#-----------------------------------------------------------------------------------------------------------------------------------
# Prefix that must be used by all repo options that allow multiple configurations
use constant CFGDEF_PREFIX_REPO                                     => 'repo';

# Repository General
use constant CFGOPT_REPO_PATH                                       => CFGDEF_PREFIX_REPO . '-path';
    push @EXPORT, qw(CFGOPT_REPO_PATH);

# Repository Host
use constant CFGOPT_REPO_HOST                                       => CFGDEF_PREFIX_REPO . '-host';
use constant CFGOPT_REPO_HOST_CMD                                   => CFGOPT_REPO_HOST . '-cmd';
    push @EXPORT, qw(CFGOPT_REPO_HOST_CMD);

# Stanza options
#-----------------------------------------------------------------------------------------------------------------------------------
# Determines how many databases can be configured
use constant CFGDEF_INDEX_PG                                        => 8;
    push @EXPORT, qw(CFGDEF_INDEX_PG);

# Prefix that must be used by all db options that allow multiple configurations
use constant CFGDEF_PREFIX_PG                                       => 'pg';
    push @EXPORT, qw(CFGDEF_PREFIX_PG);

# Set default PostgreSQL cluster
use constant CFGOPT_PG_HOST                                         => CFGDEF_PREFIX_PG . '-host';
use constant CFGOPT_PG_HOST_CMD                                     => CFGOPT_PG_HOST . '-cmd';
    push @EXPORT, qw(CFGOPT_PG_HOST_CMD);

####################################################################################################################################
# Option definition constants - defines, types, sections, etc.
####################################################################################################################################

# Command defines
#-----------------------------------------------------------------------------------------------------------------------------------
use constant CFGDEF_LOG_FILE                                        => 'log-file';
    push @EXPORT, qw(CFGDEF_LOG_FILE);
use constant CFGDEF_LOG_LEVEL_DEFAULT                               => 'log-level-default';
    push @EXPORT, qw(CFGDEF_LOG_LEVEL_DEFAULT);
use constant CFGDEF_LOCK_REQUIRED                                   => 'lock-required';
    push @EXPORT, qw(CFGDEF_LOCK_REQUIRED);
use constant CFGDEF_LOCK_REMOTE_REQUIRED                            => 'lock-remote-required';
    push @EXPORT, qw(CFGDEF_LOCK_REMOTE_REQUIRED);

use constant CFGDEF_LOCK_TYPE                                       => 'lock-type';
    push @EXPORT, qw(CFGDEF_LOCK_TYPE);
use constant CFGDEF_LOCK_TYPE_NONE                                  => 'none';

use constant CFGDEF_PARAMETER_ALLOWED                               => 'parameter-allowed';
    push @EXPORT, qw(CFGDEF_PARAMETER_ALLOWED);

# Option defines
#-----------------------------------------------------------------------------------------------------------------------------------
use constant CFGDEF_ALLOW_LIST                                      => 'allow-list';
    push @EXPORT, qw(CFGDEF_ALLOW_LIST);
use constant CFGDEF_ALLOW_RANGE                                     => 'allow-range';
    push @EXPORT, qw(CFGDEF_ALLOW_RANGE);
use constant CFGDEF_DEFAULT                                         => 'default';
    push @EXPORT, qw(CFGDEF_DEFAULT);
use constant CFGDEF_DEFAULT_LITERAL                                 => 'default-literal';
    push @EXPORT, qw(CFGDEF_DEFAULT_LITERAL);

# Group options together to share common configuration
use constant CFGDEF_GROUP                                           => 'group';
    push @EXPORT, qw(CFGDEF_GROUP);

use constant CFGDEF_BETA                                            => 'beta';
    push @EXPORT, qw(CFGDEF_BETA);
use constant CFGDEF_INDEX                                           => 'index';
    push @EXPORT, qw(CFGDEF_INDEX);
use constant CFGDEF_INHERIT                                         => 'inherit';
    push @EXPORT, qw(CFGDEF_INHERIT);
use constant CFGDEF_INTERNAL                                        => 'internal';
    push @EXPORT, qw(CFGDEF_INTERNAL);
use constant CFGDEF_DEPRECATE                                       => 'deprecate';
    push @EXPORT, qw(CFGDEF_DEPRECATE);
use constant CFGDEF_NEGATE                                          => 'negate';
    push @EXPORT, qw(CFGDEF_NEGATE);
use constant CFGDEF_COMMAND                                         => 'command';
    push @EXPORT, qw(CFGDEF_COMMAND);
use constant CFGDEF_COMMAND_ROLE                                    => 'command-role';
    push @EXPORT, qw(CFGDEF_COMMAND_ROLE);
use constant CFGDEF_REQUIRED                                        => 'required';
    push @EXPORT, qw(CFGDEF_REQUIRED);
use constant CFGDEF_RESET                                           => 'reset';
    push @EXPORT, qw(CFGDEF_RESET);
use constant CFGDEF_SECTION                                         => 'section';
    push @EXPORT, qw(CFGDEF_SECTION);
use constant CFGDEF_SECURE                                          => 'secure';
    push @EXPORT, qw(CFGDEF_SECURE);
use constant CFGDEF_TYPE                                            => 'type';
    push @EXPORT, qw(CFGDEF_TYPE);

# Option types
#-----------------------------------------------------------------------------------------------------------------------------------
use constant CFGDEF_TYPE_BOOLEAN                                    => 'boolean';
    push @EXPORT, qw(CFGDEF_TYPE_BOOLEAN);
use constant CFGDEF_TYPE_HASH                                       => 'hash';
    push @EXPORT, qw(CFGDEF_TYPE_HASH);
use constant CFGDEF_TYPE_INTEGER                                    => 'integer';
    push @EXPORT, qw(CFGDEF_TYPE_INTEGER);
use constant CFGDEF_TYPE_LIST                                       => 'list';
    push @EXPORT, qw(CFGDEF_TYPE_LIST);
use constant CFGDEF_TYPE_PATH                                       => 'path';
    push @EXPORT, qw(CFGDEF_TYPE_PATH);
use constant CFGDEF_TYPE_STRING                                     => 'string';
    push @EXPORT, qw(CFGDEF_TYPE_STRING);
use constant CFGDEF_TYPE_SIZE                                       => 'size';
    push @EXPORT, qw(CFGDEF_TYPE_SIZE);
use constant CFGDEF_TYPE_TIME                                       => 'time';
    push @EXPORT, qw(CFGDEF_TYPE_TIME);

# Option config sections
#-----------------------------------------------------------------------------------------------------------------------------------
use constant CFGDEF_SECTION_GLOBAL                                  => 'global';
    push @EXPORT, qw(CFGDEF_SECTION_GLOBAL);
use constant CFGDEF_SECTION_STANZA                                  => 'stanza';
    push @EXPORT, qw(CFGDEF_SECTION_STANZA);

####################################################################################################################################
# Load configuration
####################################################################################################################################
use YAML::XS qw(LoadFile);

# Required so booleans are not read-only
local $YAML::XS::Boolean = "JSON::PP";

my $rhConfig = LoadFile(dirname(dirname($0)) . '/src/build/config/config.yaml');
my $rhCommandDefine = $rhConfig->{'command'};
my $rhOptionGroupDefine = $rhConfig->{'optionGroup'};
my $rhConfigDefine = $rhConfig->{'option'};

####################################################################################################################################
# Fix errors introduced by YAML::XS::LoadFile. This is typically fixed by setting local $YAML::XS::Boolean = "JSON::PP", but older
# Debian/Ubuntu versions do not support this fix. Some booleans get set read only and others also end up as empty strings. There is
# no apparent pattern to what gets broken so it is important to be on the lookout for strange output when adding new options.
#
# ??? For now this code is commented out since packages for older Debians can be built using backports. It is being preserved just
# in case it is needed before the migration to C is complete.
####################################################################################################################################
# sub optionDefineFixup
# {
#     my $strKey = shift;
#     my $rhDefine = shift;
#
#     # Fix read-only required values so they are writable
#     if (defined($rhDefine->{&CFGDEF_REQUIRED}))
#     {
#         my $value = $rhDefine->{&CFGDEF_REQUIRED} ? true : false;
#         delete($rhDefine->{&CFGDEF_REQUIRED});
#         $rhDefine->{&CFGDEF_REQUIRED} = $value;
#     }
#
#     # If the default is an empty string set to false. This must be a mangled boolean since empty strings are not valid defaults.
#     if (defined($rhDefine->{&CFGDEF_DEFAULT}) && $rhDefine->{&CFGDEF_DEFAULT} eq '')
#     {
#         delete($rhDefine->{&CFGDEF_DEFAULT});
#         $rhDefine->{&CFGDEF_DEFAULT} = false;
#     }
# }
#
# # Fix all options
# foreach my $strKey (sort(keys(%{$rhConfigDefine})))
# {
#     my $rhOption = $rhConfigDefine->{$strKey};
#     optionDefineFixup($strKey, $rhOption);
#
#     # Fix all option commands
#     if (ref($rhOption->{&CFGDEF_COMMAND}))
#     {
#         foreach my $strCommand (sort(keys(%{$rhOption->{&CFGDEF_COMMAND}})))
#         {
#             optionDefineFixup("$strKey-$strCommand", $rhOption->{&CFGDEF_COMMAND}{$strCommand});
#         }
#     }
# }

####################################################################################################################################
# Process command define defaults
####################################################################################################################################
foreach my $strCommand (sort(keys(%{$rhCommandDefine})))
{
    # Commands are external by default
    if (!defined($rhCommandDefine->{$strCommand}{&CFGDEF_INTERNAL}))
    {
        $rhCommandDefine->{$strCommand}{&CFGDEF_INTERNAL} = false;
    }

    # Log files are created by default
    if (!defined($rhCommandDefine->{$strCommand}{&CFGDEF_LOG_FILE}))
    {
        $rhCommandDefine->{$strCommand}{&CFGDEF_LOG_FILE} = true;
    }

    # Default log level is INFO
    if (!defined($rhCommandDefine->{$strCommand}{&CFGDEF_LOG_LEVEL_DEFAULT}))
    {
        $rhCommandDefine->{$strCommand}{&CFGDEF_LOG_LEVEL_DEFAULT} = INFO;
    }

    # Default lock required is false
    if (!defined($rhCommandDefine->{$strCommand}{&CFGDEF_LOCK_REQUIRED}))
    {
        $rhCommandDefine->{$strCommand}{&CFGDEF_LOCK_REQUIRED} = false;
    }

    # Default lock remote required is false
    if (!defined($rhCommandDefine->{$strCommand}{&CFGDEF_LOCK_REMOTE_REQUIRED}))
    {
        $rhCommandDefine->{$strCommand}{&CFGDEF_LOCK_REMOTE_REQUIRED} = false;
    }

    # Lock type must be set if a lock is required
    if (!defined($rhCommandDefine->{$strCommand}{&CFGDEF_LOCK_TYPE}))
    {
        # Is a lock type required?
        if ($rhCommandDefine->{$strCommand}{&CFGDEF_LOCK_REQUIRED})
        {
            confess &log(ERROR, "lock type is required for command '${strCommand}'");
        }

        $rhCommandDefine->{$strCommand}{&CFGDEF_LOCK_TYPE} = CFGDEF_LOCK_TYPE_NONE;
    }
    else
    {
        if ($rhCommandDefine->{$strCommand}{&CFGDEF_LOCK_REQUIRED} &&
            $rhCommandDefine->{$strCommand}{&CFGDEF_LOCK_TYPE} eq CFGDEF_LOCK_TYPE_NONE)
        {
            confess &log(ERROR, "lock type is required for command '${strCommand}' and cannot be 'none'");
        }
    }

    # Default parameter allowed is false
    if (!defined($rhCommandDefine->{$strCommand}{&CFGDEF_PARAMETER_ALLOWED}))
    {
        $rhCommandDefine->{$strCommand}{&CFGDEF_PARAMETER_ALLOWED} = false;
    }

    # All commands have the default role
    if (!defined($rhCommandDefine->{$strCommand}{&CFGDEF_COMMAND_ROLE}{&CFGCMD_ROLE_MAIN}))
    {
        $rhCommandDefine->{$strCommand}{&CFGDEF_COMMAND_ROLE}{&CFGCMD_ROLE_MAIN} = {};
    }
}

####################################################################################################################################
# Process option define defaults
####################################################################################################################################
foreach my $strKey (sort(keys(%{$rhConfigDefine})))
{
    my $rhOption = $rhConfigDefine->{$strKey};

    # If the define is a scalar then copy the entire define from the referenced option
    if (defined($rhConfigDefine->{$strKey}{&CFGDEF_INHERIT}))
    {
        # Make a copy in case there are overrides that need to be applied after inheriting
        my $hConfigDefineOverride = dclone($rhConfigDefine->{$strKey});

        # Copy the option being inherited from
        $rhConfigDefine->{$strKey} = dclone($rhConfigDefine->{$rhConfigDefine->{$strKey}{&CFGDEF_INHERIT}});

        # No need to copy the inheritance key
        delete($rhConfigDefine->{$strKey}{&CFGDEF_INHERIT});

        # It makes no sense to inherit deprecations - they must be specified for each option
        delete($rhConfigDefine->{$strKey}{&CFGDEF_DEPRECATE});

        # Apply overrides
        foreach my $strOptionDef (sort(keys(%{$hConfigDefineOverride})))
        {
            $rhConfigDefine->{$strKey}{$strOptionDef} = $hConfigDefineOverride->{$strOptionDef};
        }

        # Update option variable with new hash reference
        $rhOption = $rhConfigDefine->{$strKey}
    }

    # If command is not specified then the option is valid for all commands except version and help
    if (!defined($rhOption->{&CFGDEF_COMMAND}))
    {
        foreach my $strCommand (sort(keys(%{$rhCommandDefine})))
        {
            next if $strCommand eq CFGCMD_HELP || $strCommand eq CFGCMD_VERSION;

            $rhOption->{&CFGDEF_COMMAND}{$strCommand} = {};
        }
    }
    # Else if the command section is a scalar then copy the section from the referenced option
    elsif (defined($rhConfigDefine->{$strKey}{&CFGDEF_COMMAND}) && !ref($rhConfigDefine->{$strKey}{&CFGDEF_COMMAND}))
    {
        $rhConfigDefine->{$strKey}{&CFGDEF_COMMAND} =
            dclone($rhConfigDefine->{$rhConfigDefine->{$strKey}{&CFGDEF_COMMAND}}{&CFGDEF_COMMAND});
    }

    # If the allow list is a scalar then copy the list from the referenced option
    if (defined($rhConfigDefine->{$strKey}{&CFGDEF_ALLOW_LIST}) && !ref($rhConfigDefine->{$strKey}{&CFGDEF_ALLOW_LIST}))
    {
        $rhConfigDefine->{$strKey}{&CFGDEF_ALLOW_LIST} =
            dclone($rhConfigDefine->{$rhConfigDefine->{$strKey}{&CFGDEF_ALLOW_LIST}}{&CFGDEF_ALLOW_LIST});
    }

    # Default type is string
    if (!defined($rhConfigDefine->{$strKey}{&CFGDEF_TYPE}))
    {
        &log(ASSERT, "type is required for option '${strKey}'");
    }

    # Default required is true
    if (!defined($rhConfigDefine->{$strKey}{&CFGDEF_REQUIRED}))
    {
        $rhConfigDefine->{$strKey}{&CFGDEF_REQUIRED} = true;
    }

    # Default internal is false
    if (!defined($rhConfigDefine->{$strKey}{&CFGDEF_INTERNAL}))
    {
        $rhConfigDefine->{$strKey}{&CFGDEF_INTERNAL} = false;
    }

    # All boolean config options can be negated. Boolean command-line options must be marked for negation individually.
    if ($rhConfigDefine->{$strKey}{&CFGDEF_TYPE} eq CFGDEF_TYPE_BOOLEAN && defined($rhConfigDefine->{$strKey}{&CFGDEF_SECTION}))
    {
        $rhConfigDefine->{$strKey}{&CFGDEF_NEGATE} = true;
    }

    # Default for negation is false
    if (!defined($rhConfigDefine->{$strKey}{&CFGDEF_NEGATE}))
    {
        $rhConfigDefine->{$strKey}{&CFGDEF_NEGATE} = false;
    }

    # All config options can be reset
    if (defined($rhConfigDefine->{$strKey}{&CFGDEF_SECTION}))
    {
        $rhConfigDefine->{$strKey}{&CFGDEF_RESET} = true;
    }
    elsif (!defined($rhConfigDefine->{$strKey}{&CFGDEF_RESET}))
    {
        $rhConfigDefine->{$strKey}{&CFGDEF_RESET} = false;
    }

    # By default options are not secure
    if (!defined($rhConfigDefine->{$strKey}{&CFGDEF_SECURE}))
    {
        $rhConfigDefine->{$strKey}{&CFGDEF_SECURE} = false;
    }

    # All size and time options must have an allow range
    if (($rhConfigDefine->{$strKey}{&CFGDEF_TYPE} eq CFGDEF_TYPE_TIME ||
         $rhConfigDefine->{$strKey}{&CFGDEF_TYPE} eq CFGDEF_TYPE_SIZE) &&
         !(defined($rhConfigDefine->{$strKey}{&CFGDEF_ALLOW_RANGE}) || defined($rhConfigDefine->{$strKey}{&CFGDEF_ALLOW_LIST})))
    {
        confess &log(ASSERT, "int/size/time option '${strKey}' must have allow range or list");
    }

    # Ensure all commands are valid
    foreach my $strCommand (sort(keys(%{$rhConfigDefine->{$strKey}{&CFGDEF_COMMAND}})))
    {
        if (!defined($rhCommandDefine->{$strCommand}))
        {
            confess &log(ASSERT, "invalid command '${strCommand}'");
        }
    }
}

# Generate valid command roles for each option
foreach my $strOption (sort(keys(%{$rhConfigDefine})))
{
    my $rhOption = $rhConfigDefine->{$strOption};

    # Generate valid command roles for each command in the option
    foreach my $strCommand (sort(keys(%{$rhOption->{&CFGDEF_COMMAND}})))
    {
        # If command roles are defined in the option command override then check that they are valid
        if (defined($rhOption->{&CFGDEF_COMMAND}{$strCommand}{&CFGDEF_COMMAND_ROLE}))
        {
            foreach my $strCommandRole (sort(keys(%{$rhOption->{&CFGDEF_COMMAND}{$strCommand}{&CFGDEF_COMMAND_ROLE}})))
            {
                if (!defined($rhCommandDefine->{$strCommand}{&CFGDEF_COMMAND_ROLE}{$strCommandRole}))
                {
                    confess &log(
                        ASSERT, "option '${strOption}', command '${strCommand}' has invalid command role '${strCommandRole}'");
                }
            }
        }
        # Else if the option has command roles defined then use the intersection of command roles with the command
        elsif (defined($rhOption->{&CFGDEF_COMMAND_ROLE}))
        {
            foreach my $strCommandRole (sort(keys(%{$rhOption->{&CFGDEF_COMMAND_ROLE}})))
            {
                if (defined($rhCommandDefine->{$strCommand}{&CFGDEF_COMMAND_ROLE}{$strCommandRole}))
                {
                    $rhOption->{&CFGDEF_COMMAND}{$strCommand}{&CFGDEF_COMMAND_ROLE}{$strCommandRole} = {};
                }
            }
        }
        # Else copy the command roles from the command
        else
        {
            foreach my $strCommandRole (sort(keys(%{$rhCommandDefine->{$strCommand}{&CFGDEF_COMMAND_ROLE}})))
            {
                $rhOption->{&CFGDEF_COMMAND}{$strCommand}{&CFGDEF_COMMAND_ROLE}{$strCommandRole} = {};
            }
        }
    }

    # Remove option command roles so they don't accidentally get used in processing (since they were copied to option commands)
    delete($rhOption->{&CFGDEF_COMMAND_ROLE});
}

####################################################################################################################################
# Get option definition
####################################################################################################################################
sub cfgDefine
{
    return dclone($rhConfigDefine);
}

push @EXPORT, qw(cfgDefine);

####################################################################################################################################
# Get command definition
####################################################################################################################################
sub cfgDefineCommand
{
    return dclone($rhCommandDefine);
}

push @EXPORT, qw(cfgDefineCommand);

####################################################################################################################################
# Get option group definition
####################################################################################################################################
sub cfgDefineOptionGroup
{
    return dclone($rhOptionGroupDefine);
}

push @EXPORT, qw(cfgDefineOptionGroup);

####################################################################################################################################
# Get list of all commands
####################################################################################################################################
sub cfgDefineCommandList
{
    # Return sorted list
    return (sort(keys(%{$rhCommandDefine})));
}

push @EXPORT, qw(cfgDefineCommandList);

####################################################################################################################################
# Get list of all option types
####################################################################################################################################
sub cfgDefineOptionTypeList
{
    my $rhOptionTypeMap;

    # Get unique list of types
    foreach my $strOption (sort(keys(%{$rhConfigDefine})))
    {
        my $strOptionType = $rhConfigDefine->{$strOption}{&CFGDEF_TYPE};

        if (!defined($rhOptionTypeMap->{$strOptionType}))
        {
            $rhOptionTypeMap->{$strOptionType} = true;
        }
    };

    # Return sorted list
    return (sort(keys(%{$rhOptionTypeMap})));
}

push @EXPORT, qw(cfgDefineOptionTypeList);

1;
