import { buffer } from 'node:stream/consumers';

const getContentType = (fileName = '') => ({
    'zip': 'application/zip'
})[fileName.split('.').at(-1)] ?? 'text/plain';

export const BOUNDARY = '--AppFrontCliFormBoundary';

export const fetch = ({http, https, fs, console, cli = {}}) => {
    const userAgent = `${cli.bin}/${cli.version} (${process.platform}; ${process.arch}; ${process.versions.node})`;
    return async (url, opts = {}) => {
        if (typeof url === 'string') {
            url = new URL(url);
        }
        const options = {};
        const client = url.protocol === 'https:' ? https : http;

        // TLS
        if (opts.tls) {
            if (opts.tls.key && opts.tls.cert) {
                console.debug(`Connecting to ${url.hostname} with PEM key and certificate`);
                options.key = await fs.readFile(opts.tls.key);
                options.cert = await fs.readFile(opts.tls.cert);
            } else if (opts.tls.p12 && opts.tls.password) {
                console.debug(`Connecting to ${url.hostname} with DER certificate bundle protected by password`);
                options.pfx = await fs.readFile(opts.tls.p12);
                options.passphrase = opts.tls.password;
            }
        }

        // Request method
        options.method = opts.method ?? 'GET';

        // Request headers
        options.headers = {
            'User-Agent': userAgent,
            'Accept': 'application/json, text/plain;q=0.9, */*;q=0.8'
        };
        if (opts.headers) {
            Object.assign(options.headers, opts.headers);
        }

        // Request
        return new Promise((resolve, reject) => {
            console.debug(`Making request to ${options.method} ${String(url)}`);
            const req = client.request(url, options, res => {
                // Response headers
                const headersForLogs = [];
                const headers = new Headers();
                for (const [key, val] of Object.entries(res.headers ?? {})) {
                    headers.append(key, val);
                    headersForLogs.push(`${key}: ${val}`);
                }

                console.debug(
                    `Response from ${String(url)}\n` +
                    `${res.statusCode} ${res.statusMessage}\n` +
                    headersForLogs.join('\n')
                );

                // Response data
                let dataResolve, dataReject;
                const dataPromise = new Promise((resolve, reject) => {
                    dataResolve = resolve;
                    dataReject = reject;
                });
                const chunks = [];
                res.on('error', dataReject);
                res.on('data', chunk => {
                    chunks.push(chunk);
                });
                res.on('end', () => {
                    const data = Buffer.concat(chunks);
                    console.debug(data.toString('utf8'));
                    dataResolve(data);
                });

                // Response object
                resolve({
                    ok: res.statusCode >= 200 && (res.statusCode < 300),
                    status: res.statusCode,
                    statusText: res.statusMessage,
                    headers: headers,
                    json: () => dataPromise.then(data => JSON.parse(data)),
                    text: () => dataPromise.then(data => data.toString('utf8')),
                    bytes: () => dataPromise,
                });
            });
            req.on('error', err => {
                reject(err);
            });
            if (opts.body instanceof FormData) {
                req.setHeader('Content-Type', 'multipart/form-data; boundary=' + BOUNDARY);
                (async () => {
                    for (const [key, val] of opts.body.entries()) {
                        req.write(`--${BOUNDARY}\r\n`);
                        if (val instanceof File) {
                            req.write(`Content-Disposition: form-data; name="${key}"; filename="${val.name}"\r\n`);
                            req.write(`Content-Type: ${getContentType(val.name)}\r\n\r\n`);
                            req.write(await buffer(val.stream()));
                            req.write('\r\n');
                        } else {
                            req.write(`Content-Disposition: form-data; name="${key}";\r\n\r\n`);
                            req.write(String(val) + '\r\n');
                        }
                    }
                    req.end(`--${BOUNDARY}--\r\n`);
                })().catch(err => {
                    req.emit('error', err);
                });
            } else if (opts.body instanceof Buffer) {
                req.end(opts.body);
            } else if (typeof opts.body === 'string') {
                req.end(opts.body);
            } else if (typeof opts.body === 'object') {
                req.end(JSON.stringify(opts.body));
            } else if (typeof opts.body === 'undefined') {
                req.end();
            } else {
                req.end(String(opts.body));
            }
        });
    };
};
