const MamiArgs = (argName, input, builder) => {
    if(input !== undefined && typeof input !== 'object')
        throw `${argName} must be an object or undefined`;
    if(typeof builder !== 'function')
        throw 'builder must be a function';

    const args = new Map;

    builder(name => {
        if(typeof name !== 'string')
            throw 'name must be a string';

        const checkDefined = () => {
            if(args.has(name))
                throw `${name} has already been defined`;
        };
        checkDefined();

        let created = false;
        const checkCreated = () => {
            if(created)
                throw 'argument has already been defined';
        };

        const info = {
            name: name,
            type: undefined,
            filter: undefined,
            constraint: undefined,
            fallback: undefined,
            throw: undefined,
            required: undefined,
            min: undefined,
            max: undefined,
        };

        const blueprint = {
            type: value => {
                if(typeof value !== 'string' && !Array.isArray(value))
                    throw 'type must be a javascript type or array of valid string values';

                checkCreated();

                info.type = value;
                return blueprint;
            },
            default: value => {
                checkCreated();
                info.fallback = value;

                if(info.type === undefined && info.constraint === undefined)
                    info.type = typeof info.fallback;

                return blueprint;
            },
            filter: value => {
                if(typeof value !== 'function')
                    throw 'filter must be a function';

                checkCreated();

                info.filter = value;
                return blueprint;
            },
            constraint: value => {
                if(typeof value !== 'function')
                    throw 'constraint must be a function';

                checkCreated();

                info.constraint = value;
                return blueprint;
            },
            throw: value => {
                checkCreated();
                info.throw = value === undefined || value === true;
                return blueprint;
            },
            required: value => {
                checkCreated();
                info.required = value === undefined || value === true;

                if(info.required && info.throw === undefined)
                    info.throw = true;

                return blueprint;
            },
            min: value => {
                checkCreated();
                if(typeof value !== 'number')
                    throw 'value must be a number';

                info.min = value;
                return blueprint;
            },
            max: value => {
                checkCreated();
                if(typeof value !== 'number')
                    throw 'value must be a number';

                info.max = value;
                return blueprint;
            },
            done: () => {
                checkCreated();
                checkDefined();

                args.set(name, info);
            },
        };

        return blueprint;
    });

    const inputIsNull = input === null;
    if(inputIsNull)
        input = {};

    const output = {};

    for(const [name, info] of args) {
        let value = info.fallback;

        if(info.name in input) {
            const defaultOrThrow = ex => {
                if(info.throw)
                    throw ex;
                value = info.fallback;
            };

            value = input[info.name];

            if(info.type !== undefined) {
                if(Array.isArray(info.type)) {
                    if(!info.type.includes(value))
                        defaultOrThrow(`${info.name} must match an enum value`);
                } else {
                    const type = typeof value;
                    let resolved = false;

                    if(type !== info.type) {
                        if(type === 'string') {
                            if(info.type === 'number') {
                                value = parseFloat(value);
                                resolved = true;

                                if(info.min !== undefined && value < info.min)
                                    value = info.min;
                                else if(info.max !== undefined && value > info.max)
                                    value = info.max;
                            } else if(info.type === 'boolean') {
                                value = !!value;
                                resolved = true;
                            }
                        } else if(info.type === 'string') {
                            value = value.toString();
                            resolved = true;
                        }
                    } else resolved = true;

                    if(!resolved)
                        defaultOrThrow(`${info.name} must be of type ${info.type}`);
                }
            }

            if(info.constraint !== undefined && !info.constraint(value))
                defaultOrThrow(`${info.name} did not fit within constraints`);

            // you could have a devious streak with this one cuz the checks aren't rerun
            // but surely you wouldn't fuck yourself over like that
            if(info.filter !== undefined)
                value = info.filter(value);
        } else if(info.required) {
            if(inputIsNull)
                throw `${argName} must be a non-null object`;

            throw `${argName} is missing required key ${info.name}`;
        }

        output[info.name] = value;
    }

    return output;
};
