// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Web client side actor message handler.
import { assert } from '//resources/js/assert.js';
import { ActorTaskPauseReason, ActorTaskState, ActorTaskStopReason } from '../../glic_api/glic_api.js';
import { ObservableValue as ObservableValueImpl, Subject } from '../../observable.js';
import { convertTabContextResultFromPrivate, convertTabDataFromPrivate } from '../client/glic_api_client.js';
import { rgbaImageToBlob } from '../client/image_utils.js';
import { ActorClientDef, ConfirmationRequestErrorReason, GmailOtpOptInErrorReason, SelectAutofillSuggestionsDialogErrorReason, SelectCredentialDialogErrorReason } from './actor_types.js';
// Implements actor-specific methods on GlicBrowserHost.
export class GlicBrowserHostActor {
    actorWebClientMessageHandler;
    userConfirmationDialogRequestSubject = new Subject();
    selectCredentialDialogRequestSubject = new Subject();
    navigationConfirmationRequestSubject = new Subject();
    actorSender;
    actorTaskState = new Map();
    selectAutofillSuggestionsDialogRequestSubject = new Subject();
    selectGmailOtpOptInRequestSubject = new Subject();
    journalHost;
    actOnWebCapabilityValue = ObservableValueImpl.withNoValue();
    actorTaskListRowClickedSubject = new Subject();
    constructor() {
        this.actorWebClientMessageHandler = new ActorWebClientMessageHandler(this);
    }
    initializeActor(initialState, router, actorRemote, actorReceiver) {
        if (actorRemote === undefined || actorReceiver === undefined ||
            !initialState.enableActInFocusedTab) {
            this.createTask = undefined;
            this.performActions = undefined;
            this.cancelActions = undefined;
            this.stopActorTask = undefined;
            this.pauseActorTask = undefined;
            this.resumeActorTask = undefined;
            this.interruptActorTask = undefined;
            this.uninterruptActorTask = undefined;
            this.getActOnWebCapability = undefined;
            this.createActorTab = undefined;
            this.actorTaskListRowClicked = undefined;
            this.getJournalHost = undefined;
            return;
        }
        this.actorSender = router.newRemote(actorRemote);
        router.newReceiver(actorReceiver, this.actorWebClientMessageHandler, ActorClientDef);
        this.journalHost = new GlicBrowserHostJournalImpl(this.actorSender);
        if (!initialState.enableGmailOtpOptIn) {
            this.selectGmailOtpOptInRequestHandler = undefined;
        }
    }
    setActorTaskState(taskId, state) {
        this.getActorTaskState(taskId).assignAndSignal(state);
        if (state === ActorTaskState.STOPPED) {
            this.actorTaskState.delete(taskId);
        }
    }
    selectUserConfirmationDialogRequestHandler() {
        return this.userConfirmationDialogRequestSubject;
    }
    selectCredentialDialogRequestHandler() {
        return this.selectCredentialDialogRequestSubject;
    }
    selectNavigationConfirmationRequestHandler() {
        return this.navigationConfirmationRequestSubject;
    }
    selectGmailOtpOptInRequestHandler() {
        return this.selectGmailOtpOptInRequestSubject;
    }
    autofillSuggestionDialogOnFormPresented(taskId, params) {
        this.actorSender?.requestNoResponse('autofillSuggestionDialogOnFormPresented', { taskId, params });
    }
    autofillSuggestionDialogOnFormPreviewChanged(taskId, params) {
        this.actorSender?.requestNoResponse('autofillSuggestionDialogOnFormPreviewChanged', { taskId, params });
    }
    autofillSuggestionDialogOnFormConfirmed(taskId, params) {
        this.actorSender?.requestNoResponse('autofillSuggestionDialogOnFormConfirmed', { taskId, params });
    }
    async getContextForActorFromTab(tabId, options) {
        assert(this.actorSender);
        const result = await this.actorSender.requestWithResponse('getContextForActorFromTab', { tabId, options });
        return convertTabContextResultFromPrivate(result.tabContextResult);
    }
    async createTask(taskOptions) {
        assert(this.actorSender);
        const result = await this.actorSender.requestWithResponse('createTask', { taskOptions });
        return result.taskId;
    }
    async performActions(actions) {
        assert(this.actorSender);
        const result = await this.actorSender.requestWithResponse('performActions', { actions });
        return result.actionsResult;
    }
    async cancelActions(taskId) {
        assert(this.actorSender);
        const response = await this.actorSender.requestWithResponse('cancelActions', { taskId });
        return response.result;
    }
    stopActorTask(taskId, stopReason) {
        this.actorSender?.requestNoResponse('stopActorTask', {
            taskId: taskId ?? 0,
            stopReason: stopReason ?? ActorTaskStopReason.TASK_COMPLETE,
        });
    }
    pauseActorTask(taskId, pauseReason, tabId) {
        this.actorSender?.requestNoResponse('pauseActorTask', {
            taskId,
            pauseReason: pauseReason ?? ActorTaskPauseReason.PAUSED_BY_MODEL,
            tabId: tabId ?? '',
        });
    }
    async resumeActorTask(taskId, tabContextOptions) {
        assert(this.actorSender);
        const response = await this.actorSender.requestWithResponse('resumeActorTask', { taskId, tabContextOptions });
        return convertTabContextResultFromPrivate(response.resumeActorTaskResult);
    }
    interruptActorTask(taskId, interruptReason) {
        this.actorSender?.requestNoResponse('interruptActorTask', {
            taskId,
            interruptReason,
        });
    }
    uninterruptActorTask(taskId) {
        this.actorSender?.requestNoResponse('uninterruptActorTask', {
            taskId,
        });
    }
    getActorTaskState(taskId) {
        const stateObs = this.actorTaskState.get(taskId);
        if (stateObs) {
            return stateObs;
        }
        // TODO(mcnee): The client could pass an id that will never have
        // state updates (e.g. the task already finished and we cleared the old
        // observable in setActorTaskState). Consider removing these cases from the
        // map when all subscribers are removed.
        const newObs = ObservableValueImpl.withNoValue();
        this.actorTaskState.set(taskId, newObs);
        return newObs;
    }
    async createActorTab(taskId, options) {
        assert(this.actorSender);
        const result = await this.actorSender.requestWithResponse('createActorTab', { taskId, options });
        if (!result.tabData) {
            throw new Error('createActorTab: failed');
        }
        return convertTabDataFromPrivate(result.tabData);
    }
    getActOnWebCapability() {
        return this.actOnWebCapabilityValue;
    }
    actorTaskListRowClicked() {
        return this.actorTaskListRowClickedSubject;
    }
    getJournalHost() {
        assert(this.journalHost);
        return this.journalHost;
    }
}
// Handles postMessage messages from the host.
export class ActorWebClientMessageHandler {
    actorHost;
    constructor(actorHost) {
        this.actorHost = actorHost;
    }
    notifyActorTaskStateChanged(payload) {
        this.actorHost.setActorTaskState(payload.taskId, payload.state);
    }
    async requestToShowDialog(payload) {
        const request = payload.request;
        return new Promise(resolve => {
            if (!this.actorHost.selectCredentialDialogRequestSubject
                .hasActiveSubscription()) {
                // Since there is no subscriber, respond to the browser immediately as
                // if no credential is selected.
                window.console.warn('GlicWebClient: no subscriber for' +
                    ' selectCredentialDialogRequest()!');
                resolve({
                    response: {
                        taskId: request.taskId,
                        errorReason: SelectCredentialDialogErrorReason.DIALOG_PROMISE_NO_SUBSCRIBER,
                    },
                });
                return;
            }
            const iconsGetter = new Map();
            for (const [id, image] of payload.request.icons.entries()) {
                let promise;
                iconsGetter.set(id, () => {
                    if (!promise) {
                        promise = Promise.resolve(rgbaImageToBlob(image));
                    }
                    return promise;
                });
            }
            const credentials = request.credentials.map((credential) => {
                const getIcon = iconsGetter.get(credential.sourceSiteOrApp);
                const accountPicture = credential.accountPicture;
                const getAccountPicture = accountPicture ?
                    () => Promise.resolve(rgbaImageToBlob(accountPicture)) :
                    undefined;
                return {
                    ...credential,
                    getIcon,
                    getAccountPicture,
                };
            });
            const requestWithCallback = {
                ...request,
                credentials,
                onDialogClosed: resolve,
            };
            this.actorHost.selectCredentialDialogRequestSubject.next(requestWithCallback);
        });
    }
    requestToShowConfirmationDialog(payload) {
        return new Promise(resolve => {
            if (!this.actorHost.userConfirmationDialogRequestSubject
                .hasActiveSubscription()) {
                // Since there is no subscriber, respond to the browser immediately as
                // if the user denied the request.
                window.console.warn('GlicWebClient: no subscriber for ' +
                    'userConfirmationDialogRequest()!');
                resolve({
                    response: {
                        permissionGranted: false,
                        errorReason: ConfirmationRequestErrorReason.REQUEST_PROMISE_NO_SUBSCRIBER,
                    },
                });
                return;
            }
            const requestWithCallback = {
                ...payload.request,
                onDialogClosed: resolve,
            };
            this.actorHost.userConfirmationDialogRequestSubject.next(requestWithCallback);
        });
    }
    requestToConfirmNavigation(payload) {
        return new Promise(resolve => {
            if (!this.actorHost.navigationConfirmationRequestSubject
                .hasActiveSubscription()) {
                // Since there is no subscriber, respond to the browser immediately as
                // if the user denied the request.
                window.console.warn('GlicWebClient: no subscriber for ' +
                    'navigationConfirmationRequest()!');
                resolve({
                    response: {
                        errorReason: ConfirmationRequestErrorReason.REQUEST_PROMISE_NO_SUBSCRIBER,
                    },
                });
                return;
            }
            const requestWithCallback = {
                ...payload.request,
                onConfirmationDecision: resolve,
            };
            this.actorHost.navigationConfirmationRequestSubject.next(requestWithCallback);
        });
    }
    async requestToShowAutofillSuggestionsDialog(payload) {
        const request = payload.request;
        return new Promise(resolve => {
            if (!this.actorHost.selectAutofillSuggestionsDialogRequestSubject
                .hasActiveSubscription()) {
                resolve({
                    response: {
                        taskId: request.taskId,
                        errorReason: SelectAutofillSuggestionsDialogErrorReason
                            .DIALOG_PROMISE_NO_SUBSCRIBER,
                        selectedSuggestions: [],
                    },
                });
                return;
            }
            const requestWithCallback = {
                ...request,
                formFillingRequests: request.formFillingRequests.map(formFillingRequest => ({
                    ...formFillingRequest,
                    suggestions: formFillingRequest.suggestions.map(suggestion => {
                        const icon = suggestion.icon;
                        const getIcon = icon ?
                            () => Promise.resolve(rgbaImageToBlob(icon)) :
                            undefined;
                        return { ...suggestion, getIcon };
                    }),
                })),
                onDialogClosed: (result) => {
                    const response = {
                        ...result.response,
                        taskId: request.taskId,
                    };
                    resolve({
                        response: response,
                    });
                },
                onFormPresented: (params) => {
                    this.actorHost.autofillSuggestionDialogOnFormPresented(request.taskId, params);
                },
                onFormPreviewChanged: (params) => {
                    this.actorHost.autofillSuggestionDialogOnFormPreviewChanged(request.taskId, params);
                },
                onFormConfirmed: (params) => {
                    this.actorHost.autofillSuggestionDialogOnFormConfirmed(request.taskId, params);
                },
            };
            this.actorHost.selectAutofillSuggestionsDialogRequestSubject.next(requestWithCallback);
        });
    }
    requestToShowGmailOtpOptInDialog(payload) {
        return new Promise(resolve => {
            if (!this.actorHost.selectGmailOtpOptInRequestSubject
                .hasActiveSubscription()) {
                window.console.warn('GlicWebClient: no subscriber for ' +
                    'selectGmailOtpOptInRequestHandler()!');
                resolve({
                    response: {
                        permissionGranted: false,
                        errorReason: GmailOtpOptInErrorReason.REQUEST_PROMISE_NO_SUBSCRIBER,
                    },
                });
                return;
            }
            const requestWithCallback = {
                ...payload.request,
                onDialogClosed: (response) => {
                    resolve({ response });
                },
            };
            this.actorHost.selectGmailOtpOptInRequestSubject.next(requestWithCallback);
        });
    }
}
export class GlicBrowserHostJournalImpl {
    sender;
    constructor(sender) {
        this.sender = sender;
    }
    beginAsyncEvent(asyncEventId, taskId, event, details) {
        this.sender.requestNoResponse('logBeginAsyncEvent', { asyncEventId, taskId, event, details });
    }
    clear() {
        this.sender.requestNoResponse('journalClear', undefined);
    }
    endAsyncEvent(asyncEventId, details) {
        this.sender.requestNoResponse('logEndAsyncEvent', { asyncEventId, details });
    }
    instantEvent(taskId, event, details) {
        this.sender.requestNoResponse('logInstantEvent', { taskId, event, details });
    }
    async snapshot(clear) {
        const snapshotResult = await this.sender.requestWithResponse('journalSnapshot', { clear });
        return snapshotResult.journal;
    }
    start(maxBytes, captureScreenshots) {
        this.sender.requestNoResponse('journalStart', { maxBytes, captureScreenshots });
    }
    stop() {
        this.sender.requestNoResponse('journalStop', undefined);
    }
    recordFeedback(positive, reason) {
        this.sender.requestNoResponse('journalRecordFeedback', { positive, reason });
    }
}
