import { Readable } from 'node:stream';
import mime from 'mime';
import Err from '@openaddresses/batch-error';
import Commands, { CommandOutputFormat, type ParsedArgs } from '../commands.js';
import { TAKList } from './types.js';
import { Type, Static } from '@sinclair/typebox';

export const Content = Type.Object({
  UID: Type.String(),
  SubmissionDateTime: Type.String(),
  Keywords: Type.Array(Type.String()),
  MIMEType: Type.String(),
  SubmissionUser: Type.String(),
  PrimaryKey: Type.String(),
  Hash: Type.String(),
  CreatorUid: Type.String(),
  Name: Type.String()
});

export const TAKList_Content = TAKList(Type.Object({
    filename: Type.String(),
    keywords: Type.Array(Type.String()),
    mimeType: Type.String(),
    name: Type.String(),
    submissionTime: Type.String(),
    submitter: Type.String(),
    uid: Type.String(),
    size: Type.Integer(),
}));

export const Config = Type.Object({
    uploadSizeLimit: Type.Integer()
})

export default class FileCommands extends Commands {
    schema = {
        list: {
            description: 'List Files',
            params: Type.Object({}),
            query: Type.Object({}),
            formats: [ CommandOutputFormat.JSON ]
        }
    }

    async cli(args: ParsedArgs): Promise<object | string> {
        if (args._[3] === 'list') {
            const list = await this.list();

            if (args.format === 'json') {
                return list;
            } else {
                return list.data.map((data) => {
                    return data.filename;
                }).join('\n');
            }
        } else {
            throw new Error('Unsupported Subcommand');
        }
    }

    async list(): Promise<Static<typeof TAKList_Content>> {
        const url = new URL(`/Marti/api/sync/search`, this.api.url);

        return await this.api.fetch(url, {
            method: 'GET'
        });
    }

    async meta(hash: string): Promise<string> {
        const url = new URL(`/Marti/sync/${encodeURIComponent(hash)}/metadata`, this.api.url);

        const res = await this.api.fetch(url, {
            method: 'GET'
        }, true);

        const body = await res.text();

        return body;
    }

    async download(hash: string): Promise<Readable> {
        const url = new URL(`/Marti/sync/content`, this.api.url);
        url.searchParams.append('hash', hash);

        const res = await this.api.fetch(url, {
            method: 'GET'
        }, true);

        // The raw fetch path skips status validation - without this check a
        // TAK Server error page would be streamed as if it were file content
        if (res.status < 200 || res.status >= 400) {
            const body = await res.text().catch(() => '');
            throw new Err(res.status, null, body || `Status Code: ${res.status}`);
        }

        return res.body;
    }

    async adminDelete(hash: string) {
        const url = new URL(`/Marti/api/files/${hash}`, this.api.url);

        return await this.api.fetch(url, {
            method: 'DELETE',
        });
    }

    async delete(hash: string) {
        const url = new URL(`/Marti/sync/delete`, this.api.url);
        url.searchParams.append('hash', hash)

        return await this.api.fetch(url, {
            method: 'DELETE',
        });
    }

    // TODO Return a Content Object

    /**
     * Update a Package that should appear in the Public Data Packages List
     */
    async uploadPackage(opts: {
        name: string;
        creatorUid: string;
        hash: string;
        keywords?: string[];
        mimetype?: string;
        groups?: string[];
    }, body: Readable | Buffer): Promise<string> {
        const url = new URL(`/Marti/sync/missionupload`, this.api.url);
        url.searchParams.append('filename', opts.name)
        url.searchParams.append('creatorUid', opts.creatorUid)
        url.searchParams.append('hash', opts.hash)

        if (opts.mimetype) {
            url.searchParams.append('mimetype', opts.mimetype)
        }

        // Needs this keywordt to appear in public list
        url.searchParams.append('keyword', 'missionpackage');

        if (opts.keywords) {
            for (const keyword of opts.keywords) {
                if (keyword.toLowerCase() === 'missionpackage') continue;
                url.searchParams.append('keyword', keyword);
            }
        }

        if (opts.groups) {
            for (const group of opts.groups) {
                // This is intentionally case sensitive due to an apparent bug in TAK server
                url.searchParams.append('Groups', group);
            }
        }

        const form = new FormData()
        const contentType = opts.mimetype || mime.getType(opts.name) || undefined;
        const file = await toBlob(body, contentType);
        form.append('assetfile', file, opts.name);

        const res = await this.api.fetch(url, {
            method: 'POST',
            body: form
        }) as string;

        return res;
    }

    /**
     * Update a Package that will not appear in the Public Data Packages List
     * used primarily for sharing files between TAK clients
     */
    async upload(opts: {
        name: string;
        contentLength: number;
        contentType?: string;
        keywords: string[];
        creatorUid: string;
        latitude?: string;
        longitude?: string;
        altitude?: string;
    }, body: Readable | Buffer): Promise<Static<typeof Content>> {
        const url = new URL(`/Marti/sync/upload`, this.api.url);
        url.searchParams.append('name', opts.name)
        url.searchParams.append('keywords', opts.keywords.join(','))
        url.searchParams.append('creatorUid', opts.creatorUid)
        if (opts.altitude) url.searchParams.append('altitude', opts.altitude);
        if (opts.longitude) url.searchParams.append('longitude', opts.longitude);
        if (opts.latitude) url.searchParams.append('latitude', opts.latitude);

        if (body instanceof Buffer) {
            body = Readable.from(body as Buffer);
        }

        const res = await this.api.fetch(url, {
            method: 'POST',
            headers: {
                'Content-Type': opts.contentType || mime.getType(opts.name) || 'application/octet-stream',
                'Content-Length': opts.contentLength
            },
            body: body as Readable
        });

        return typeof res === 'string' ? JSON.parse(res) : res;
    }

    async count() {
        const url = new URL('/Marti/api/files/metadata/count', this.api.url);

        return await this.api.fetch(url, {
            method: 'GET'
        });
    }

    async update(hash: string, opts: {
        keywords?: string[];
        expiration?: number;
    }): Promise<void> {
        if (opts.keywords !== undefined) {
            const url = new URL(`/Marti/api/sync/metadata/${encodeURIComponent(hash)}/keywords`, this.api.url);

            await this.api.fetch(url, {
                method: 'PUT',
                headers: { 'Content-Type': 'application/json' },
                body: opts.keywords
            });
        }

        if (opts.expiration !== undefined) {
            const url = new URL(`/Marti/api/sync/metadata/${encodeURIComponent(hash)}/expiration`, this.api.url);
            url.searchParams.append('expiration', String(opts.expiration));

            await this.api.fetch(url, {
                method: 'PUT',
            });
        }
    }

    async config(): Promise<Static<typeof Config>> {
        const url = new URL('/files/api/config', this.api.url);

        return await this.api.fetch(url, {
            method: 'GET'
        });
    }
}

async function toBlob(body: Readable | Buffer, type?: string): Promise<Blob> {
    if (body instanceof Buffer) {
        const bytes = new Uint8Array(body);
        return type ? new Blob([bytes], { type }) : new Blob([bytes]);
    }

    const chunks: Buffer[] = [];

    for await (const chunk of body) {
        if (chunk instanceof Buffer) {
            chunks.push(chunk);
        } else if (typeof chunk === 'string') {
            chunks.push(Buffer.from(chunk));
        } else {
            chunks.push(Buffer.from(chunk));
        }
    }

    const buff = Buffer.concat(chunks);
    const bytes = new Uint8Array(buff);
    return type ? new Blob([bytes], { type }) : new Blob([bytes]);
}
