import { equal } from 'assert';
import spawn from 'await-spawn';
import {
    ChildProcessWithoutNullStreams,
    spawn as spawnSync,
} from 'child_process';
import { JSONSchema7 } from 'json-schema';

import { app, dialog, shell } from 'electron';
import { sys } from 'typescript';
import { preferences, sapio_config_file } from './settings';
import path from 'path';
import * as Bitcoin from 'bitcoinjs-lib';
import { mkdir, readdir, readFile, writeFile } from 'fs/promises';
import { API, Result } from '../src/common/preload_interface';
import { ConstructionRounded } from '@mui/icons-material';

const memo_apis = new Map();
const memo_logos = new Map();

export class SapioWorkspace {
    name: string;
    private constructor(name: string) {
        this.name = name;
    }
    static async list_all(): Promise<string[]> {
        const file = path.join(app.getPath('userData'), 'workspaces');
        return readdir(file, { encoding: 'ascii' });
    }
    static async new(name: string): Promise<SapioWorkspace> {
        const file = path.join(
            app.getPath('userData'),
            'workspaces',
            name,
            'compiled_contracts'
        );
        const created = await mkdir(file, { recursive: true });
        return new SapioWorkspace(name);
    }

    async list_compiled_contracts(): Promise<string[]> {
        const file = path.join(
            app.getPath('userData'),
            'workspaces',
            this.name,
            'compiled_contracts'
        );
        const contracts = await readdir(file, { encoding: 'ascii' });
        return contracts;
    }
    async trash_compiled_contract(s: string): Promise<void> {
        const file = path.join(
            app.getPath('userData'),
            'workspaces',
            this.name,
            'compiled_contracts',
            s
        );
        return shell.trashItem(file);
    }
    async trash_workspace(s: string): Promise<void> {
        const file = path.join(
            app.getPath('userData'),
            'workspaces',
            this.name
        );
        return shell.trashItem(file);
    }

    workspace_location() {
        return path.join(app.getPath('userData'), 'workspaces', this.name);
    }

    contract_output_path_name(fname: string) {
        return path.join(
            app.getPath('userData'),
            'workspaces',
            this.name,
            'compiled_contracts',
            fname
        );
    }
    async contract_output_path(fname: string) {
        const file = this.contract_output_path_name(fname);
        await mkdir(file, { recursive: true });
        return file;
    }

    async read_bound_data_for(file_name: string) {
        const file = this.contract_output_path_name(file_name);
        const data = JSON.parse(
            await readFile(path.join(file, 'bound.json'), {
                encoding: 'utf-8',
            })
        );
        return data;
    }
    async read_args_for(file_name: string) {
        const file = this.contract_output_path_name(file_name);
        const args = JSON.parse(
            await readFile(path.join(file, 'args.json'), {
                encoding: 'utf-8',
            })
        );
        return args;
    }

    async read_module_for(file_name: string): Promise<string> {
        const file = this.contract_output_path_name(file_name);
        const mod = await readFile(path.join(file, 'module.json'), {
            encoding: 'utf-8',
        });
        const name = JSON.parse(mod).module;
        return name;
    }
}

class SapioCompiler {
    static async command(args: string[]): Promise<Result<string>> {
        const binary = preferences.data.sapio_cli.sapio_cli;
        const source = preferences.data.sapio_cli.preferences;
        let new_args: string[] = [];
        if (source === 'Default') {
            new_args = args;
        } else if ('File' in source) {
            new_args = ['--config', source.File, ...args];
        } else if ('Here' in source) {
            new_args = ['--config', sapio_config_file, ...args];
        } else {
            dialog.showErrorBox(
                'Improper Source',
                'This means your config file is corrupt, shutting down.'
            );
            sys.exit(1);
        }
        console.debug(['sapio'], binary, new_args);
        try {
            const ok = (await spawn(binary, new_args)).toString();
            return { ok };
        } catch (e: any) {
            return { err: e.stderr.toString() };
        }
    }

    async psbt_finalize(psbt: string): Promise<Result<string>> {
        return await SapioCompiler.command([
            'psbt',
            'finalize',
            '--psbt',
            psbt,
        ]);
    }

    async show_config(): Promise<Result<string>> {
        return await SapioCompiler.command(['configure', 'show']);
    }

    async list_contracts(workspace_name: string): Promise<Result<API>> {
        const workspace = await SapioWorkspace.new(workspace_name);
        const res = await SapioCompiler.command([
            'contract',
            'list',
            '--workspace',
            workspace.workspace_location(),
        ]);
        if ('err' in res) return res;
        const contracts = res.ok;
        const lines: Array<[string, string]> = contracts
            .trim()
            .split(/\r?\n/)
            .map((line: string) => {
                const v: string[] = line.split(' -- ')!;
                equal(v.length, 2);
                return v as [string, string];
            });

        const apis_p = Promise.all(
            lines.map(([name, key]: [string, string]): Promise<JSONSchema7> => {
                if (memo_apis.has(key)) {
                    return Promise.resolve(memo_apis.get(key));
                } else {
                    return SapioCompiler.command([
                        'contract',
                        'api',
                        '--key',
                        key,
                        '--workspace',
                        workspace.workspace_location(),
                    ]).then((v) => {
                        if ('err' in v) return v;
                        const api = JSON.parse(v.ok);
                        memo_apis.set(key, api);
                        return api;
                    });
                }
            })
        );
        const logos_p = Promise.all(
            lines.map(
                ([name, key]: [string, string]): Promise<Result<string>> => {
                    if (memo_logos.has(key)) {
                        return Promise.resolve(memo_logos.get(key));
                    } else {
                        return SapioCompiler.command([
                            'contract',
                            'logo',
                            '--key',
                            key,
                            '--workspace',
                            workspace.workspace_location(),
                        ])
                            .then((logo: Result<string>) => {
                                return 'ok' in logo
                                    ? { ok: logo.ok.trim() }
                                    : logo;
                            })
                            .then((logo: Result<string>) => {
                                if ('err' in logo) return logo;
                                memo_logos.set(key, logo);
                                return logo;
                            });
                    }
                }
            )
        );
        const [apis, logos] = await Promise.all([apis_p, logos_p]);

        const results: API = {};
        equal(lines.length, apis.length);
        equal(lines.length, logos.length);
        for (let i = 0; i < lines.length; ++i) {
            const [name, key] = lines[i]!;
            const api = apis[i]!;
            const logo = logos[i]!;
            if ('err' in logo) return logo;
            results[key] = {
                name,
                key,
                api,
                logo: logo.ok,
            };
        }
        return { ok: results };
    }
    async load_contract_file_name(
        workspace_name: string,
        file: string
    ): Promise<{ ok: null }> {
        const workspace = await SapioWorkspace.new(workspace_name);
        const child = await SapioCompiler.command([
            'contract',
            'load',
            '--file',
            file,
            '--workspace',
            workspace.workspace_location(),
        ]);
        console.log(`child stdout:\n${JSON.stringify(child)}`);
        return { ok: null };
    }

    async create_contract(
        workspace_name: string,
        which: string,
        txn: string | null,
        args: string
    ): Promise<Result<string | null>> {
        const workspace = await SapioWorkspace.new(workspace_name);
        let create, created, bound;
        const args_h = Bitcoin.crypto.sha256(Buffer.from(args)).toString('hex');
        // Unique File Name of Time + Args + Module
        const fname = `${which.substring(0, 16)}-${args_h.substring(
            0,
            16
        )}-${new Date().getTime()}`;
        const file = await workspace.contract_output_path(fname);
        const write_str = (to: string, data: string) =>
            writeFile(path.join(file, to), data, { encoding: 'utf-8' });
        const w_arg = write_str('args.json', args);
        const w_mod = write_str(
            'module.json',
            JSON.stringify({ module: which })
        );
        const sc = await sapio.show_config();
        if ('err' in sc) return Promise.reject('Error getting config');
        const w_settings = write_str('settings.json', sc.ok);

        Promise.all([w_arg, w_mod, w_settings]);

        try {
            create = await SapioCompiler.command([
                'contract',
                'create',
                '--key',
                which,
                args,
                '--workspace',
                workspace.workspace_location(),
            ]);
        } catch (e) {
            console.debug('Failed to Create', which, args);
            return { ok: null };
        }
        if ('err' in create) {
            write_str('create_error.json', JSON.stringify(create));
            return create;
        }
        created = create.ok;
        const w_create = write_str('create.json', create.ok);
        Promise.all([w_create]);
        let bind;
        try {
            const bind_args = ['contract', 'bind', '--base64_psbt'];
            if (txn) bind_args.push('--txn', txn);
            bind_args.push(created);
            bind = await SapioCompiler.command(bind_args);
        } catch (e: any) {
            console.debug(created);
            console.log('Failed to bind', e.toString());
            return { ok: null };
        }
        if ('err' in bind) {
            console.log(['bind'], typeof bind, bind);
            write_str('bind_error.json', JSON.stringify(bind));
            return bind;
        }
        const w_bound = write_str('bound.json', bind.ok);
        await w_bound;
        console.debug(bound);
        return bind;
    }
}

export const sapio = new SapioCompiler();

let g_emulator: null | ChildProcessWithoutNullStreams = null;
let g_emulator_log = '';

export function get_emulator_log(): string {
    return g_emulator_log;
}
export function start_sapio_oracle(): ChildProcessWithoutNullStreams | null {
    if (g_emulator !== null) return g_emulator;
    const oracle = preferences.data.local_oracle;
    if (oracle !== 'Disabled' && 'Enabled' in oracle) {
        const binary = preferences.data.sapio_cli.sapio_cli;
        const seed = oracle.Enabled.file;
        const iface = oracle.Enabled.interface;
        const emulator = spawnSync(binary, ['emulator', 'server', seed, iface]);
        if (emulator) {
            let quit = '';
            emulator.stderr.on('data', (data) => {
                quit += `${data}`;
            });
            emulator.on('exit', (code) => {
                if (quit !== '') {
                    console.error('Emulator Oracle Error', quit);
                    sys.exit();
                }
            });
            emulator.stdout.on('data', (data) => {
                g_emulator_log += `${data}`;
                console.log(`stdout: ${data}`);
            });
        }
        g_emulator = emulator;
        return g_emulator;
    }
    return null;
}

export function kill_emulator() {
    console.log('Killing Emulator');
    g_emulator_log = '';
    g_emulator?.kill();
    g_emulator = null;
}