'use strict';

const Os = require('os');

const Somever = require('@hapi/somever');
const Validate = require('@hapi/validate');


const internals = {};


exports.symbol = Symbol('hapi-response');


exports.apply = function (type, options, ...message) {

    const result = internals[type].validate(options);

    if (result.error) {
        throw new Error(`Invalid ${type} options ${message.length ? '(' + message.join(' ') + ')' : ''} ${result.error.annotate()}`);
    }

    return result.value;
};


exports.enable = function (options) {

    const settings = options ? Object.assign({}, options) : {};         // Shallow cloned

    if (settings.security === true) {
        settings.security = {};
    }

    if (settings.cors === true) {
        settings.cors = {};
    }

    return settings;
};

exports.versionMatch = (version, range) => Somever.match(version, range, { includePrerelease: true });

internals.access = Validate.object({
    entity: Validate.valid('user', 'app', 'any'),
    scope: [false, Validate.array().items(Validate.string()).single().min(1)]
});


internals.auth = Validate.alternatives([
    Validate.string(),
    internals.access.keys({
        mode: Validate.valid('required', 'optional', 'try'),
        strategy: Validate.string(),
        strategies: Validate.array().items(Validate.string()).min(1),
        access: Validate.array().items(internals.access.min(1)).single().min(1),
        payload: [
            Validate.valid('required', 'optional'),
            Validate.boolean()
        ]
    })
        .without('strategy', 'strategies')
        .without('access', ['scope', 'entity'])
]);


internals.event = Validate.object({
    method: Validate.array().items(Validate.function()).single(),
    options: Validate.object({
        before: Validate.array().items(Validate.string()).single(),
        after: Validate.array().items(Validate.string()).single(),
        bind: Validate.any(),
        sandbox: Validate.valid('server', 'plugin'),
        timeout: Validate.number().integer().min(1)
    })
        .default({})
});


internals.exts = Validate.array()
    .items(internals.event.keys({ type: Validate.string().required() })).single();


internals.failAction = Validate.alternatives([
    Validate.valid('error', 'log', 'ignore'),
    Validate.function()
])
    .default('error');


internals.routeBase = Validate.object({
    app: Validate.object().allow(null),
    auth: internals.auth.allow(false),
    bind: Validate.object().allow(null),
    cache: Validate.object({
        expiresIn: Validate.number(),
        expiresAt: Validate.string(),
        privacy: Validate.valid('default', 'public', 'private'),
        statuses: Validate.array().items(Validate.number().integer().min(200)).min(1).single().default([200, 204]),
        otherwise: Validate.string().default('no-cache')
    })
        .allow(false)
        .default(),
    compression: Validate.object()
        .pattern(/.+/, Validate.object())
        .default(),
    cors: Validate.object({
        origin: Validate.array().min(1).allow('ignore').default(['*']),
        maxAge: Validate.number().default(86400),
        headers: Validate.array().items(Validate.string()).default(['Accept', 'Authorization', 'Content-Type', 'If-None-Match']),
        additionalHeaders: Validate.array().items(Validate.string()).default([]),
        exposedHeaders: Validate.array().items(Validate.string()).default(['WWW-Authenticate', 'Server-Authorization']),
        additionalExposedHeaders: Validate.array().items(Validate.string()).default([]),
        credentials: Validate.boolean().when('origin', { is: 'ignore', then: false }).default(false),
        preflightStatusCode: Validate.valid(200, 204).default(200)
    })
        .allow(false, true)
        .default(false),
    ext: Validate.object({
        onPreAuth: Validate.array().items(internals.event).single(),
        onCredentials: Validate.array().items(internals.event).single(),
        onPostAuth: Validate.array().items(internals.event).single(),
        onPreHandler: Validate.array().items(internals.event).single(),
        onPostHandler: Validate.array().items(internals.event).single(),
        onPreResponse: Validate.array().items(internals.event).single(),
        onPostResponse: Validate.array().items(internals.event).single()
    })
        .default({}),
    files: Validate.object({
        relativeTo: Validate.string().pattern(/^([\/\.])|([A-Za-z]:\\)|(\\\\)/).default('.')
    })
        .default(),
    json: Validate.object({
        replacer: Validate.alternatives(Validate.function(), Validate.array()).allow(null).default(null),
        space: Validate.number().allow(null).default(null),
        suffix: Validate.string().allow(null).default(null),
        escape: Validate.boolean().default(false)
    })
        .default(),
    log: Validate.object({
        collect: Validate.boolean().default(false)
    })
        .default(),
    payload: Validate.object({
        output: Validate.valid('data', 'stream', 'file').default('data'),
        parse: Validate.boolean().allow('gunzip').default(true),
        multipart: Validate.object({
            output: Validate.valid('data', 'stream', 'file', 'annotated').required()
        })
            .default(false)
            .allow(true, false),
        allow: Validate.array().items(Validate.string()).single(),
        override: Validate.string(),
        protoAction: Validate.valid('error', 'remove', 'ignore').default('error'),
        maxBytes: Validate.number().integer().positive().default(1024 * 1024),
        maxParts: Validate.number().integer().positive().default(1000),
        uploads: Validate.string().default(Os.tmpdir()),
        failAction: internals.failAction,
        timeout: Validate.number().integer().positive().allow(false).default(10 * 1000),
        defaultContentType: Validate.string().default('application/json'),
        compression: Validate.object()
            .pattern(/.+/, Validate.object())
            .default()
    })
        .default(),
    plugins: Validate.object(),
    response: Validate.object({
        disconnectStatusCode: Validate.number().integer().min(400).default(499),
        emptyStatusCode: Validate.valid(200, 204).default(204),
        failAction: internals.failAction,
        modify: Validate.boolean(),
        options: Validate.object(),
        ranges: Validate.boolean().default(true),
        sample: Validate.number().min(0).max(100).when('modify', { then: Validate.forbidden() }),
        schema: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(true, false),
        status: Validate.object().pattern(/\d\d\d/, Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(true, false))
    })
        .default(),
    security: Validate.object({
        hsts: Validate.alternatives([
            Validate.object({
                maxAge: Validate.number(),
                includeSubdomains: Validate.boolean(),
                includeSubDomains: Validate.boolean(),
                preload: Validate.boolean()
            }),
            Validate.boolean(),
            Validate.number()
        ])
            .default(15768000),
        xframe: Validate.alternatives([
            Validate.boolean(),
            Validate.valid('sameorigin', 'deny'),
            Validate.object({
                rule: Validate.valid('sameorigin', 'deny', 'allow-from'),
                source: Validate.string()
            })
        ])
            .default('deny'),
        xss: Validate.valid('enabled', 'disabled', false).default('disabled'),
        noOpen: Validate.boolean().default(true),
        noSniff: Validate.boolean().default(true),
        referrer: Validate.alternatives([
            Validate.boolean().valid(false),
            Validate.valid('', 'no-referrer', 'no-referrer-when-downgrade',
                'unsafe-url', 'same-origin', 'origin', 'strict-origin',
                'origin-when-cross-origin', 'strict-origin-when-cross-origin')
        ])
            .default(false)
    })
        .allow(null, false, true)
        .default(false),
    state: Validate.object({
        parse: Validate.boolean().default(true),
        failAction: internals.failAction
    })
        .default(),
    timeout: Validate.object({
        socket: Validate.number().integer().positive().allow(false),
        server: Validate.number().integer().positive().allow(false).default(false)
    })
        .default(),
    validate: Validate.object({
        headers: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, true),
        params: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, true),
        query: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, false, true),
        payload: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, false, true),
        state: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, false, true),
        failAction: internals.failAction,
        errorFields: Validate.object(),
        options: Validate.object().default(),
        validator: Validate.object()
    })
        .default()
});


internals.server = Validate.object({
    address: Validate.string().hostname(),
    app: Validate.object().allow(null),
    autoListen: Validate.boolean(),
    cache: Validate.allow(null),                                 // Validated elsewhere
    compression: Validate.object({
        minBytes: Validate.number().min(1).integer().default(1024)
    })
        .allow(false)
        .default(),
    debug: Validate.object({
        request: Validate.array().items(Validate.string()).single().allow(false).default(['implementation']),
        log: Validate.array().items(Validate.string()).single().allow(false)
    })
        .allow(false)
        .default(),
    host: Validate.string().hostname().allow(null),
    info: Validate.object({
        remote: Validate.boolean().default(false)
    })
        .default({}),
    listener: Validate.any(),
    load: Validate.object({
        sampleInterval: Validate.number().integer().min(0).default(0)
    })
        .unknown()
        .default(),
    mime: Validate.object().empty(null).default(),
    operations: Validate.object({
        cleanStop: Validate.boolean().default(true)
    })
        .default(),
    plugins: Validate.object(),
    port: Validate.alternatives([
        Validate.number().integer().min(0),          // TCP port
        Validate.string().pattern(/\//),               // Unix domain socket
        Validate.string().pattern(/^\\\\\.\\pipe\\/)   // Windows named pipe
    ])
        .allow(null),
    query: Validate.object({
        parser: Validate.function()
    })
        .default(),
    router: Validate.object({
        isCaseSensitive: Validate.boolean().default(true),
        stripTrailingSlash: Validate.boolean().default(false)
    })
        .default(),
    routes: internals.routeBase.default(),
    state: Validate.object(),                                    // Cookie defaults
    tls: Validate.alternatives([
        Validate.object().allow(null),
        Validate.boolean()
    ]),
    uri: Validate.string().pattern(/[^/]$/)
});


internals.vhost = Validate.alternatives([
    Validate.string().hostname(),
    Validate.array().items(Validate.string().hostname()).min(1)
]);


internals.handler = Validate.alternatives([
    Validate.function(),
    Validate.object().length(1)
]);


internals.route = Validate.object({
    method: Validate.string().pattern(/^[a-zA-Z0-9!#\$%&'\*\+\-\.^_`\|~]+$/).required(),
    path: Validate.string().required(),
    rules: Validate.object(),
    vhost: internals.vhost,

    // Validated in route construction

    handler: Validate.any(),
    options: Validate.any(),
    config: Validate.any()               // Backwards compatibility
})
    .without('config', 'options');


internals.pre = [
    Validate.function(),
    Validate.object({
        method: Validate.alternatives(Validate.string(), Validate.function()).required(),
        assign: Validate.string(),
        mode: Validate.valid('serial', 'parallel'),
        failAction: internals.failAction
    })
];


internals.routeConfig = internals.routeBase.keys({
    description: Validate.string(),
    id: Validate.string(),
    isInternal: Validate.boolean(),
    notes: [
        Validate.string(),
        Validate.array().items(Validate.string())
    ],
    pre: Validate.array().items(...internals.pre.concat(Validate.array().items(...internals.pre).min(1))),
    tags: [
        Validate.string(),
        Validate.array().items(Validate.string())
    ]
});


internals.cacheConfig = Validate.alternatives([
    Validate.function(),
    Validate.object({
        name: Validate.string().invalid('_default'),
        shared: Validate.boolean(),
        provider: [
            Validate.function(),
            {
                constructor: Validate.function().required(),
                options: Validate.object({
                    partition: Validate.string().default('hapi-cache')
                })
                    .unknown()      // Catbox client validates other keys
                    .default({})
            }
        ],
        engine: Validate.object()
    })
        .xor('provider', 'engine')
]);


internals.cache = Validate.array().items(internals.cacheConfig).min(1).single();


internals.cachePolicy = Validate.object({
    cache: Validate.string().allow(null).allow(''),
    segment: Validate.string(),
    shared: Validate.boolean()
})
    .unknown();                     // Catbox policy validates other keys


internals.method = Validate.object({
    bind: Validate.object().allow(null),
    generateKey: Validate.function(),
    cache: internals.cachePolicy
});


internals.methodObject = Validate.object({
    name: Validate.string().required(),
    method: Validate.function().required(),
    options: Validate.object()
});


internals.register = Validate.object({
    once: true,
    routes: Validate.object({
        prefix: Validate.string().pattern(/^\/.+/),
        vhost: internals.vhost
    })
        .default({})
});


internals.semver = Validate.string();


internals.plugin = internals.register.keys({
    options: Validate.any(),
    plugin: Validate.object({
        register: Validate.function().required(),
        name: Validate.string().when('pkg.name', { is: Validate.exist(), otherwise: Validate.required() }),
        version: Validate.string(),
        multiple: Validate.boolean().default(false),
        dependencies: [
            Validate.array().items(Validate.string()).single(),
            Validate.object().pattern(/.+/, internals.semver)
        ],
        once: true,
        requirements: Validate.object({
            hapi: Validate.string(),
            node: Validate.string()
        })
            .default(),
        pkg: Validate.object({
            name: Validate.string(),
            version: Validate.string().default('0.0.0')
        })
            .unknown()
            .default({})
    })
        .unknown()
})
    .without('once', 'options')
    .unknown();


internals.rules = Validate.object({
    validate: Validate.object({
        schema: Validate.alternatives(Validate.object(), Validate.array()).required(),
        options: Validate.object()
            .default({ allowUnknown: true })
    })
});
