/**
 * Copyright 2025 Angus.Fenying <fenying@litert.org>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import * as Shared from '../shared';
import type * as dC from './Client.decl';
import type * as dT from '../transporters';
import { EventEmitter } from 'node:events';
import { AbstractTvChannelV2, encoder } from '../shared/Channel.impl';
import * as v2 from '../shared/Encodings/v2';
import { buffer2json, /* debugLog */ } from '../shared/Utils';

let channelIdCounter = Date.now();

class TvJsonApiClient<TApis extends Shared.IObject> extends EventEmitter implements dC.IClient<TApis> {

    private _channel: TvClientChannel | null = null;

    private _connecting: Promise<TvClientChannel> | null = null;

    public constructor(
        private readonly _connector: dT.IConnector,
        public timeout: number,
        private readonly _streamManagerFactory: Shared.IStreamManagerFactory,
    ) {

        super();
    }

    public get streams(): Shared.IStreamManager {

        const ret = this._channel?.streams;

        if (!ret) {

            throw new Shared.errors.channel_inactive();
        }

        return ret;
    }

    private readonly _onChannelClose = (): void => {

        this._channel?.off('close', this._onChannelClose)
            .off('push_message', this._onMessage);

        this._channel = null;
        this.emit('close');
    };

    private readonly _onMessage = (msg: Buffer[]): void => {

        this.emit('push_message', msg);
    };

    private readonly _onError = (e: unknown): void => {

        this.emit('error', e);
    };

    public get transporter(): Shared.ITransporter  | null {

        return this._channel?.transporter ?? null;
    }

    public get ended(): boolean {

        return false !== this._channel?.ended;
    }

    public get finished(): boolean {

        return false !== this._channel?.finished;
    }

    public get writable(): boolean {

        return false !== this._channel?.writable;
    }

    protected async _getChannel(): Promise<TvClientChannel> {

        if (this._channel?.transporter?.writable) {

            // debugLog('televoke', 'Reusing existing channel.');
            return this._channel;
        }

        if (this._connecting) {

            // debugLog('televoke', 'Waiting for previous connection to be established.');
            return this._connecting;
        }

        try {

            // debugLog('televoke', 'Attempting to connect...');
            return this._channel = await (this._connecting = this._connect());
        }
        catch (e) {

            // debugLog('televoke', 'Attempts to connect failed.');
            this._channel = null;
            this._connecting = null;
            throw e;
        }
        finally {

            // debugLog('televoke', 'Attempts to connect finished.');
            this._connecting = null;
        }
    }

    private async _connect(): Promise<TvClientChannel> {

        this._channel = new TvClientChannel(
            await this._connector.connect() as any,
            this._streamManagerFactory,
            this.timeout
        )
            .on('close', this._onChannelClose)
            .on('error', this._onError)
            .on('push_message', this._onMessage);

        return this._channel;
    }

    public async invoke(
        name: unknown,
        ...args: any[]
    ): Promise<any> {

        try {

            const resp = await (await this._getChannel()).apiCall(name as string, JSON.stringify(args));

            return buffer2json(resp);
        }
        catch (e) {

            if (e instanceof Shared.errors.app_error) {

                let info: unknown;

                try {

                    info = JSON.parse(e.message);
                }
                catch {

                    throw e;
                }

                throw new Shared.TvErrorResponse(info);
            }

            throw e;
        }
    }

    public close(): void {

        this._channel?.close();
        // this._onChannelClose();
    }

    public async ping(msg?: Buffer | string): Promise<Buffer> {

        return (await this._getChannel()).ping(msg);
    }

    public async connect(): Promise<void> {

        await this._getChannel();
    }

    public async sendBinaryChunk(streamId: number, index: number, chunk: Buffer): Promise<void> {

        return (await this._getChannel()).sendBinaryChunk(streamId, index, chunk);
    }
}

class TvClientChannel extends AbstractTvChannelV2 {

    public constructor(
        transporter: dT.ITransporter & Shared.ITransporter,
        streamManagerFactory: Shared.IStreamManagerFactory,
        timeout: number = 60_000,
    ) {

        super(channelIdCounter++, transporter, timeout, streamManagerFactory);
    }

    public apiCall(
        name: string,
        argsBody: Buffer | string | Buffer[],
    ): Promise<Buffer[]> {

        if (!this.writable) {

            return Promise.reject(new Shared.errors.channel_inactive());
        }

        const seq = this._seqCounter++;

        (this.transporter as dT.ITransporter).write(encoder.encode({
            'typ': v2.EPacketType.REQUEST,
            'cmd': v2.ECommand.API_CALL,
            'seq': seq,
            'ct': {
                'name': name,
                'body': typeof argsBody === 'string' ? Buffer.from(argsBody) : argsBody
            }
        } satisfies v2.IApiRequestPacket));

        return new Promise((resolve, reject) => {

            this._setTimeout(v2.ECommand.API_CALL, seq, (resp) => {

                if (resp.ct instanceof Shared.TelevokeError) {

                    reject(resp.ct);
                    return;
                }

                resolve(resp.ct as Buffer[]);
            });
        });
    }
}

/**
 * Create a JSON-RPC client instance, over televoke2 protocol.
 *
 * @param connector             The connector instance.
 * @param streamManagerFactory  The stream manager factory function. [default: createSharedStreamManagerFactory()]
 * @param timeout               The default timeout value of commands, in milliseconds. [default: 60000]
 * @returns                     The client instance.
 */
export function createJsonApiClient<TApis extends Shared.IObject>(
    connector: dT.IConnector,
    streamManagerFactory: Shared.IStreamManagerFactory = Shared.createSharedStreamManagerFactory(),
    timeout: number = 60_000,
): dC.IClient<TApis> {

    return new TvJsonApiClient<TApis>(connector, timeout, streamManagerFactory);
}
