import { Args, Command } from '@oclif/core';
import { FetchError, fromBasedString, fromRandom, getFileSharingCode, getFileSharingLink, getInviteSharingCode, getInviteSharingLink, ResponseError, SDK, toBasedString, } from '@trebuchet/sdk-nodejs';
import chalk from 'chalk';
import { DateTime } from 'luxon';
import { webcrypto as crypto } from 'node:crypto';
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
export const REQUEST_PARTIAL_ID = Args.string({
    description: 'Request ID (case-insensitive prefix match)',
    required: true,
});
export const TRANSFER_PARTIAL_ID = Args.string({
    description: 'Transfer ID (case-insensitive prefix match)',
    required: true,
});
export class BaseCommand extends Command {
    args;
    // @ts-expect-error auth defined in init()
    auth;
    flags;
    kek;
    key;
    parsedArgv;
    // @ts-expect-error SDK defined in init()
    sdk;
    // @ts-expect-error userConfig defined in init()
    userConfig = {};
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    async catch(err) {
        if (err instanceof ResponseError) {
            this.logToStderr(`[${err.response.status}] ${err.response.statusText}`);
            const body = await err.response.json();
            this.logJson(body);
            this.exit(1);
        }
        else if (err instanceof FetchError) {
            this.log(err.message);
            this.exit(1);
        }
        else {
            this.error(err, { code: '1' });
        }
    }
    dateToString(val, fallback) {
        if (!val) {
            return fallback || 'Unknown Date';
        }
        const ts = DateTime.fromJSDate(val);
        switch (this.userConfig.dateFormat) {
            case 'DATETIME_FULL':
                return ts.toLocaleString(DateTime.DATETIME_FULL);
            default:
                return ts.toLocaleString(DateTime.DATETIME_FULL);
        }
    }
    async ensureConfigDirExists(...paths) {
        const fileRoot = join(this.config.dataDir, 'cache', ...paths);
        await mkdir(fileRoot, { recursive: true });
        return fileRoot;
    }
    async getInvite(publicId) {
        const root = await this.getRequestsCache();
        const invitePath = join(root, `${publicId}.json`);
        const inviteData = JSON.parse(await readFile(invitePath, { encoding: 'utf8' }));
        const data = await crypto.subtle.decrypt({
            iv: fromBasedString('base64-url', inviteData.iv),
            name: 'AES-GCM',
            tagLength: 128,
        }, this.key, fromBasedString('base64-url', inviteData.edata));
        return JSON.parse(new TextDecoder().decode(data));
    }
    async getRequestsCache() {
        return this.ensureConfigDirExists(this.auth.user.publicId, 'requests');
    }
    async getTransfer(publicId) {
        const root = await this.getTransfersCache();
        const transfer = join(root, `${publicId}.json`);
        const transferData = JSON.parse(await readFile(transfer, { encoding: 'utf8' }));
        const data = await crypto.subtle.decrypt({
            iv: fromBasedString('base64-url', transferData.iv),
            name: 'AES-GCM',
            tagLength: 128,
        }, this.key, fromBasedString('base64-url', transferData.edata));
        return JSON.parse(new TextDecoder().decode(data));
    }
    async getTransfersCache() {
        return this.ensureConfigDirExists(this.auth.user.publicId, 'transfers');
    }
    async init() {
        await super.init();
        const { args, argv, flags } = await this.parse({
            args: this.ctor.args,
            baseFlags: super.ctor.baseFlags,
            enableJsonFlag: this.ctor.enableJsonFlag,
            flags: this.ctor.flags,
            strict: this.ctor.strict,
        });
        this.flags = flags;
        this.args = args;
        this.parsedArgv = argv;
        this.userConfig = {
            apiKey: this.config.scopedEnvVar('API_KEY') || 'NONE',
            baseUrl: this.config.scopedEnvVar('BASE_URL') || 'https://transfer.hexiosec.com',
            // @ts-expect-error should be valid anyway
            dateFormat: this.config.scopedEnvVar('DATE_FORMAT') || 'DATETIME_FULL',
            defaultDownloads: Number.parseInt(this.config.scopedEnvVar('DEFAULT_DOWNLOADS') || '5', 10),
            defaultExpiryDays: Number.parseInt(this.config.scopedEnvVar('DEFAULT_EXPIRY_DAYS') || '7', 10),
        };
        this.sdk = new SDK(this.userConfig.baseUrl + '/api', this.userConfig.apiKey);
        this.auth = await this.sdk.getAuthState();
        if (this.auth.authenticated) {
            this.kek = await crypto.subtle.importKey('raw', fromBasedString('hex', this.auth.user.kek), 'AES-KW', false, [
                'wrapKey',
                'unwrapKey',
            ]);
            const root = await this.ensureConfigDirExists(this.auth.user.publicId);
            const keyPath = join(root, 'keys.json');
            try {
                await stat(keyPath);
                const keyData = JSON.parse(await readFile(keyPath, { encoding: 'utf8' }));
                this.key = await crypto.subtle.unwrapKey('raw', fromBasedString('base64-url', keyData.ekey), await this.kek, {
                    name: 'AES-KW',
                }, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
            }
            catch (error) {
                if (error.code === 'ENOENT') {
                    this.key = await crypto.subtle.generateKey({
                        length: 256,
                        name: 'AES-GCM',
                    }, true, ['encrypt', 'decrypt']);
                    const ekey = toBasedString('base64-url', await crypto.subtle.wrapKey('raw', this.key, this.kek, {
                        name: 'AES-KW',
                    }));
                    await writeFile(keyPath, JSON.stringify({ ekey }, null, 2), { encoding: 'utf8' });
                }
                else {
                    throw error;
                }
            }
        }
    }
    async logInvite(inv) {
        const link = getInviteSharingLink(this.userConfig.baseUrl, inv);
        const code = getInviteSharingCode(inv);
        let metaExists = true;
        try {
            await this.getInvite(inv.publicId);
        }
        catch {
            metaExists = false;
        }
        this.log(`ID:             ${inv.publicId}`);
        this.log(`Subject:        ${inv.subject}`);
        this.log(`Link:           ${link}`);
        this.log(`Sharing Code:   ${code}`);
        this.log(`State:          ${inv.state === 'active' ? chalk.green('active') : chalk.gray('archived')}`);
        this.log(`Created:        ${this.dateToString(inv.created)}`);
        this.log(`Deadline:       ${inv.exp ? this.dateToString(inv.exp) : 'Not set'}`);
        this.log(`Keys:           ${metaExists ? chalk.green('Available') : chalk.red('Not Available')}`);
        this.log(`Received Files: ${inv.files?.activeCount || 0}`);
        return { link, code, keys: metaExists, ...inv };
    }
    async logTransfer(file) {
        let meta;
        try {
            meta = await this.getTransfer(file.publicId);
        }
        catch {
            // Not available
        }
        const link = meta
            ? getFileSharingLink(this.userConfig.baseUrl, file, meta.fragment)
            : 'Not available';
        const code = meta ? getFileSharingCode(file, meta.fragment) : 'Not available';
        this.log(`ID:             ${file.publicId}`);
        this.log(`Description:    ${file.description}`);
        this.log(`Link:           ${link}`);
        this.log(`Sharing Code:   ${code}`);
        if (meta?.password) {
            this.log(`Password:     ${meta?.password}`);
        }
        this.log(`Created:        ${this.dateToString(file.created)}`);
        this.log(`Expires:        ${file.exp ? this.dateToString(file.exp) : 'Not set'}`);
        this.log(`Downloads:      ${file.count}/${file.max || 'unlimited'}`);
        return {
            code,
            link,
            ...file,
        };
    }
    async matchInvite(id) {
        const res = await this.sdk.invitesApi.getInvites({ matchId: this.args.id });
        if (res[0]) {
            return res[0];
        }
        throw new Error(`No sharing request found matching id "${id}"`);
    }
    async matchTransfer(id) {
        const res = await this.sdk.filesApi.getFiles({ matchId: this.args.id });
        if (res[0]) {
            return res[0];
        }
        throw new Error(`No transfers found matching id "${id}"`);
    }
    async setInvite(publicId, data) {
        const root = await this.getRequestsCache();
        const invitePath = join(root, `${publicId}.json`);
        const iv = fromRandom(12);
        const estore = {
            edata: toBasedString('base64-url', await crypto.subtle.encrypt({
                iv,
                name: 'AES-GCM',
                tagLength: 128,
            }, this.key, new TextEncoder().encode(JSON.stringify(data)))),
            iv: toBasedString('base64-url', iv),
        };
        await writeFile(invitePath, JSON.stringify(estore, null, 2));
    }
    async setTransfer(publicId, data) {
        const root = await this.getTransfersCache();
        const transferPath = join(root, `${publicId}.json`);
        const iv = fromRandom(12);
        const estore = {
            edata: toBasedString('base64-url', await crypto.subtle.encrypt({
                iv,
                name: 'AES-GCM',
                tagLength: 128,
            }, this.key, new TextEncoder().encode(JSON.stringify(data)))),
            iv: toBasedString('base64-url', iv),
        };
        await writeFile(transferPath, JSON.stringify(estore, null, 2));
    }
}
