import path from 'node:path';
import _fs from 'node:fs';
import process from 'node:process';
import jaguar from 'jaguar';
import {onezip} from 'onezip';
import inly from 'inly';
import wraptile from 'wraptile';
import currify from 'currify';
import pullout from 'pullout';
import json from 'jonny';
import ponse from 'ponse';
import {copymitter} from 'copymitter';
import _moveFiles from '@cloudcmd/move-files';
import * as CloudFunc from '#common/cloudfunc';
import root from '../root.js';
import markdown from '../markdown/index.js';
import info from './info.js';

const isUndefined = (a) => typeof a === 'undefined';
const isRootAll = (root, names) => names.some(isRootWin32(root));
const isString = (a) => typeof a === 'string';
const isFn = (a) => typeof a === 'function';
const swap = wraptile((fn, a, b) => fn(b, a));
const isWin32 = process.platform === 'win32';
const {apiURL} = CloudFunc;

const UserError = (msg) => {
    const error = Error(msg);
    
    error.code = 'EUSER';
    
    return error;
};

export default currify(({config, fs = _fs, moveFiles = _moveFiles}, request, response, next) => {
    const name = ponse.getPathName(request);
    const regExp = RegExp(`^${apiURL}`);
    const is = regExp.test(name);
    
    if (!is)
        return next();
    
    rest({fs, config, moveFiles}, request, response);
});

function rest({fs, config, moveFiles}, request, response) {
    const name = ponse.getPathName(request);
    const params = {
        request,
        response,
        name: name.replace(apiURL, '') || '/',
    };
    
    sendData(params, {fs, config, moveFiles}, (error, options, data) => {
        params.gzip = !error;
        
        if (!data) {
            data = options;
            options = {};
        }
        
        if (options.name)
            params.name = options.name;
        
        if (!isUndefined(options.gzip))
            params.gzip = options.gzip;
        
        if (options.query)
            params.query = options.query;
        
        if (error?.code)
            return ponse.sendError(error, params);
        
        if (error)
            return ponse.sendError(error.stack, params);
        
        ponse.send(data, params);
    });
}

/**
 * getting data on method and command
 *
 * @param params {name, method, body, request, response}
 * @param config {}
 * @param callback
 */
function sendData(params, {fs, config, moveFiles}, callback) {
    const p = params;
    const isMD = p.name.startsWith('/markdown');
    const rootDir = config('root');
    
    if (isMD)
        return markdown(p.name, rootDir, p.request, callback);
    
    const {method} = p.request;
    
    switch(method) {
    case 'GET':
        return onGET(params, config, callback);
    
    case 'PUT':
        return pullout(p.request)
            .then((body) => {
                onPUT({
                    name: p.name,
                    fs,
                    moveFiles,
                    config,
                    body,
                }, callback);
            })
            .catch(callback);
    }
}

function onGET(params, config, callback) {
    let cmd;
    const p = params;
    const packer = config('packer');
    const prefix = config('prefix');
    const rootDir = config('root');
    
    if (p.name[0] === '/')
        cmd = p.name.replace('/', '');
    
    if (cmd.startsWith('pack')) {
        cmd = cmd.replace(/^pack/, '');
        streamPack(root(cmd, rootDir), p.response, packer);
        
        return;
    }
    
    switch(cmd) {
    case '':
        p.data = json.stringify(info(prefix));
        
        callback(null, {name: 'api.json'}, p.data);
        break;
    
    default:
        callback({
            message: 'Not Found',
        });
        break;
    }
}

function getPackReg(packer) {
    if (packer === 'zip')
        return /\.zip$/;
    
    return /\.tar\.gz$/;
}

function streamPack(cmd, response, packer) {
    const noop = () => {};
    const filename = cmd.replace(getPackReg(packer), '');
    const dir = path.dirname(filename);
    
    const names = [
        path.basename(filename),
    ];
    
    operation('pack', packer, dir, response, names, noop);
}

function getCMD(cmd) {
    if (cmd[0] === '/')
        return cmd.slice(1);
    
    return cmd;
}

const getMoveMsg = (names) => formatMsg('move', names);

const getRenameMsg = (from, to) => {
    const msg = formatMsg('rename', {
        from,
        to,
    });
    
    return msg;
};

export const _onPUT = onPUT;

function onPUT({name, fs, moveFiles, config, body}, callback) {
    checkPut(name, body, callback);
    
    const cmd = getCMD(name);
    const files = json.parse(body);
    const rootDir = config('root');
    
    switch(cmd) {
    case 'move': {
        const {
            from,
            to,
            names,
        } = files;
        
        if (!from)
            return callback(UserError('"from" should be filled'));
        
        if (!to)
            return callback(UserError('"to" should be filled'));
        
        if (isRootAll(rootDir, [to, from]))
            return callback(getWin32RootMsg());
        
        const msg = getMoveMsg(names);
        const fn = swap(callback, msg);
        
        const fromRooted = root(from, rootDir);
        const toRooted = root(to, rootDir);
        
        return moveFiles(fromRooted, toRooted, names)
            .on('error', fn)
            .on('end', fn);
    }
    
    case 'rename':
        return rename(rootDir, files.from, files.to, fs, callback);
    
    case 'copy':
        if (!files.from || !files.names || !files.to)
            return callback(body);
        
        if (isRootAll(rootDir, [files.to, files.from]))
            return callback(getWin32RootMsg());
        
        files.from = root(files.from, rootDir);
        files.to = root(files.to, rootDir);
        
        copy(files.from, files.to, files.names, (error) => {
            const msg = formatMsg('copy', files.names);
            callback(error, msg);
        });
        break;
    
    case 'pack':
        if (!files.from)
            return callback(body);
        
        pack(files.from, files.to, files.names, config, callback);
        break;
    
    case 'extract':
        if (!files.from)
            return callback(body);
        
        extract(files.from, files.to, config, callback);
        
        break;
    
    default:
        callback();
        break;
    }
}

function rename(rootDir, from, to, fs, callback) {
    if (!from)
        return callback(UserError('"from" should be filled'));
    
    if (!to)
        return callback(UserError('"to" should be filled'));
    
    const msg = getRenameMsg(from, to);
    const fn = swap(callback, msg);
    
    const fromRooted = root(from, rootDir);
    const toRooted = root(to, rootDir);
    
    return fs.rename(fromRooted, toRooted, fn);
}

function pack(from, to, names, config, fn) {
    const rootDir = config('root');
    const packer = config('packer');
    
    from = root(from, rootDir);
    to = root(to, rootDir);
    
    if (!names) {
        names = [
            path.basename(from),
        ];
        
        from = path.dirname(from);
    }
    
    operation('pack', packer, from, to, names, fn);
}

function extract(from, to, config, fn) {
    const rootDir = config('root');
    
    from = root(from, rootDir);
    
    if (to)
        to = root(to, rootDir);
    else
        to = from.replace(/\.tar\.gz$/, '');
    
    operation('extract', config('packer'), from, to, fn);
}

function getPacker(operation, packer) {
    if (operation === 'extract')
        return inly;
    
    if (packer === 'zip')
        return onezip.pack;
    
    return jaguar.pack;
}

function operation(op, packer, from, to, names, fn) {
    if (!fn) {
        fn = names;
        names = [
            path.basename(from),
        ];
    }
    
    const packerFn = getPacker(op, packer);
    const pack = packerFn(from, to, names);
    
    pack.on('error', fn);
    
    const [name] = names;
    pack.on('progress', (count) => {
        process.stdout.write(`\r${op} "${name}": ${count}%`);
    });
    
    pack.on('end', () => {
        process.stdout.write('\n');
        
        const name = path.basename(from);
        const msg = formatMsg(op, name);
        
        fn(null, msg);
    });
}

function copy(from, to, names, fn) {
    copymitter(from, to, names)
        .on('error', fn)
        .on('progress', (count) => {
            process.stdout.write(`\r copy ${from} ${to} ${count}%`);
        })
        .on('end', () => {
            process.stdout.write('\n');
            fn();
        });
}

const isRootWin32 = currify((root, path) => {
    const isRoot = path === '/';
    const isConfig = root === '/';
    
    return isWin32 && isRoot && isConfig;
});

export const _isRootWin32 = isRootWin32;
export const _isRootAll = isRootAll;

export const _getWin32RootMsg = getWin32RootMsg;

function getWin32RootMsg() {
    const message = 'Could not copy from/to root on windows!';
    
    return Error(message);
}

function parseData(data) {
    const isObj = typeof data === 'object';
    
    if (!isObj)
        return data;
    
    return json.stringify(data);
}

export const _formatMsg = formatMsg;

function formatMsg(msg, data, status) {
    const value = parseData(data);
    return CloudFunc.formatMsg(msg, value, status);
}

function checkPut(name, body, callback) {
    if (!isString(name))
        throw Error('name should be a string!');
    
    if (!isString(body))
        throw Error('body should be a string!');
    
    if (!isFn(callback))
        throw Error('callback should be a function!');
}
