// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import * as Common from '../../core/common/common.js';
import * as Host from '../../core/host/host.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as Root from '../../core/root/root.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as NetworkTimeCalculator from '../network_time_calculator/network_time_calculator.js';

import {
  type AiAgent,
  type ExternalRequestResponse,
  ExternalRequestResponseType,
  ResponseType
} from './agents/AiAgent.js';
import {NetworkAgent, RequestContext} from './agents/NetworkAgent.js';
import type {PerformanceTraceContext} from './agents/PerformanceAgent.js';
import {NodeContext, StylingAgent} from './agents/StylingAgent.js';
import {AiConversation} from './AiConversation.js';
import {
  ConversationType,
} from './AiHistoryStorage.js';
import {getDisabledReasons} from './AiUtils.js';

interface ExternalStylingRequestParameters {
  conversationType: ConversationType.STYLING;
  prompt: string;
  selector?: string;
}

interface ExternalNetworkRequestParameters {
  conversationType: ConversationType.NETWORK;
  prompt: string;
  requestUrl: string;
}

export interface ExternalPerformanceAIConversationData {
  conversationHandler: ConversationHandler;
  conversation: AiConversation;
  selected: PerformanceTraceContext;
}

export interface ExternalPerformanceRequestParameters {
  conversationType: ConversationType.PERFORMANCE;
  prompt: string;
  data: ExternalPerformanceAIConversationData;
}

/*
* Strings that don't need to be translated at this time.
*/
const UIStringsNotTranslate = {
  /**
   * @description Error message shown when AI assistance is not enabled in DevTools settings.
   */
  enableInSettings: 'For AI features to be available, you need to enable AI assistance in DevTools settings.',
} as const;

const lockedString = i18n.i18n.lockedString;

function isAiAssistanceServerSideLoggingEnabled(): boolean {
  return !Root.Runtime.hostConfig.aidaAvailability?.disallowLogging;
}

async function inspectElementBySelector(selector: string): Promise<SDK.DOMModel.DOMNode|null> {
  const whitespaceTrimmedQuery = selector.trim();
  if (!whitespaceTrimmedQuery.length) {
    return null;
  }

  const showUAShadowDOM = Common.Settings.Settings.instance().moduleSetting('show-ua-shadow-dom').get();
  const domModels = SDK.TargetManager.TargetManager.instance().models(SDK.DOMModel.DOMModel, {scoped: true});

  const performSearchPromises =
      domModels.map(domModel => domModel.performSearch(whitespaceTrimmedQuery, showUAShadowDOM));
  const resultCounts = await Promise.all(performSearchPromises);

  // If the selector matches multiple times, this returns the first match.
  const index = resultCounts.findIndex(value => value > 0);
  if (index >= 0) {
    return await domModels[index].searchResult(0);
  }
  return null;
}

async function inspectNetworkRequestByUrl(selector: string): Promise<SDK.NetworkRequest.NetworkRequest|null> {
  const networkManagers =
      SDK.TargetManager.TargetManager.instance().models(SDK.NetworkManager.NetworkManager, {scoped: true});

  const results = networkManagers
                      .map(networkManager => {
                        let request = networkManager.requestForURL(Platform.DevToolsPath.urlString`${selector}`);
                        if (!request && selector.at(-1) === '/') {
                          request =
                              networkManager.requestForURL(Platform.DevToolsPath.urlString`${selector.slice(0, -1)}`);
                        } else if (!request && selector.at(-1) !== '/') {
                          request = networkManager.requestForURL(Platform.DevToolsPath.urlString`${selector}/`);
                        }
                        return request;
                      })
                      .filter(req => !!req);
  const request = results.at(0);

  return request ?? null;
}

let conversationHandlerInstance: ConversationHandler|undefined;

export class ConversationHandler extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
  #aiAssistanceEnabledSetting: Common.Settings.Setting<boolean>|undefined;
  #aidaClient: Host.AidaClient.AidaClient;
  #aidaAvailability?: Host.AidaClient.AidaAccessPreconditions;

  private constructor(
      aidaClient: Host.AidaClient.AidaClient, aidaAvailability?: Host.AidaClient.AidaAccessPreconditions) {
    super();
    this.#aidaClient = aidaClient;
    this.#aidaAvailability = aidaAvailability;
    this.#aiAssistanceEnabledSetting = this.#getAiAssistanceEnabledSetting();
  }

  static instance(opts?: {
    aidaClient?: Host.AidaClient.AidaClient,
    aidaAvailability?: Host.AidaClient.AidaAccessPreconditions,
    forceNew?: boolean,
  }): ConversationHandler {
    if (opts?.forceNew || conversationHandlerInstance === undefined) {
      const aidaClient = opts?.aidaClient ?? new Host.AidaClient.AidaClient();
      conversationHandlerInstance = new ConversationHandler(aidaClient, opts?.aidaAvailability ?? undefined);
    }
    return conversationHandlerInstance;
  }

  static removeInstance(): void {
    conversationHandlerInstance = undefined;
  }

  get aidaClient(): Host.AidaClient.AidaClient {
    return this.#aidaClient;
  }

  #getAiAssistanceEnabledSetting(): Common.Settings.Setting<boolean>|undefined {
    try {
      return Common.Settings.moduleSetting('ai-assistance-enabled') as Common.Settings.Setting<boolean>;
    } catch {
      return;
    }
  }

  async #getDisabledReasons(): Promise<Platform.UIString.LocalizedString[]> {
    if (this.#aidaAvailability === undefined) {
      this.#aidaAvailability = await Host.AidaClient.AidaClient.checkAccessPreconditions();
    }
    return getDisabledReasons(this.#aidaAvailability);
  }

  // eslint-disable-next-line require-yield
  async * #generateErrorResponse(message: string): AsyncGenerator<ExternalRequestResponse, ExternalRequestResponse> {
    return {
      type: ExternalRequestResponseType.ERROR,
      message,
    };
  }

  /**
   * Handles an external request using the given prompt and uses the
   * conversation type to use the correct agent.
   */
  async handleExternalRequest(
      parameters: ExternalStylingRequestParameters|ExternalNetworkRequestParameters|
      ExternalPerformanceRequestParameters,
      ): Promise<AsyncGenerator<ExternalRequestResponse, ExternalRequestResponse>> {
    try {
      this.dispatchEventToListeners(ConversationHandlerEvents.EXTERNAL_REQUEST_RECEIVED);
      const disabledReasons = await this.#getDisabledReasons();
      const aiAssistanceSetting = this.#aiAssistanceEnabledSetting?.getIfNotDisabled();
      if (!aiAssistanceSetting) {
        disabledReasons.push(lockedString(UIStringsNotTranslate.enableInSettings));
      }
      if (disabledReasons.length > 0) {
        return this.#generateErrorResponse(disabledReasons.join(' '));
      }

      this.dispatchEventToListeners(
          ConversationHandlerEvents.EXTERNAL_CONVERSATION_STARTED, parameters.conversationType);
      switch (parameters.conversationType) {
        case ConversationType.STYLING: {
          return await this.#handleExternalStylingConversation(parameters.prompt, parameters.selector);
        }
        case ConversationType.PERFORMANCE:
          return await this.#handleExternalPerformanceConversation(parameters.prompt, parameters.data);
        case ConversationType.NETWORK:
          if (!parameters.requestUrl) {
            return this.#generateErrorResponse('The url is required for debugging a network request.');
          }
          return await this.#handleExternalNetworkConversation(parameters.prompt, parameters.requestUrl);
      }
    } catch (error) {
      return this.#generateErrorResponse(error.message);
    }
  }

  async * #createAndDoExternalConversation(opts: {
    conversationType: ConversationType,
    aiAgent: AiAgent<unknown>,
    prompt: string,
    selected: NodeContext|PerformanceTraceContext|RequestContext|null,
  }): AsyncGenerator<ExternalRequestResponse, ExternalRequestResponse> {
    const {conversationType, aiAgent, prompt, selected} = opts;
    const conversation = new AiConversation(
        conversationType,
        [],
        aiAgent.sessionId,
        /* isReadOnly */ true,
        this.#aidaClient,
        undefined,
        /* isExternal */ true,
    );
    return yield* this.#doExternalConversation({conversation, prompt, selected});
  }

  async * #doExternalConversation(opts: {
    conversation: AiConversation,
    prompt: string,
    selected: NodeContext|PerformanceTraceContext|RequestContext|null,
  }): AsyncGenerator<ExternalRequestResponse, ExternalRequestResponse> {
    const {conversation, prompt, selected} = opts;
    conversation.setContext(selected);
    const generator = conversation.run(prompt);
    const devToolsLogs: object[] = [];
    for await (const data of generator) {
      if (data.type !== ResponseType.ANSWER || data.complete) {
        devToolsLogs.push(data);
      }
      if (data.type === ResponseType.CONTEXT || data.type === ResponseType.TITLE) {
        yield {
          type: ExternalRequestResponseType.NOTIFICATION,
          message: data.title,
        };
      }
      if (data.type === ResponseType.SIDE_EFFECT) {
        data.confirm(true);
      }
      if (data.type === ResponseType.ANSWER && data.complete) {
        return {
          type: ExternalRequestResponseType.ANSWER,
          message: data.text,
          devToolsLogs,
        };
      }
    }
    return {
      type: ExternalRequestResponseType.ERROR,
      message: 'Something went wrong. No answer was generated.',
    };
  }

  async #handleExternalStylingConversation(prompt: string, selector = 'body'):
      Promise<AsyncGenerator<ExternalRequestResponse, ExternalRequestResponse>> {
    const stylingAgent = new StylingAgent({
      aidaClient: this.#aidaClient,
      serverSideLoggingEnabled: isAiAssistanceServerSideLoggingEnabled(),
    });
    const node = await inspectElementBySelector(selector);
    if (node) {
      await node.setAsInspectedNode();
    }
    const selected = node ? new NodeContext(node) : null;
    return this.#createAndDoExternalConversation({
      conversationType: ConversationType.STYLING,
      aiAgent: stylingAgent,
      prompt,
      selected,
    });
  }

  async #handleExternalPerformanceConversation(prompt: string, data: ExternalPerformanceAIConversationData):
      Promise<AsyncGenerator<ExternalRequestResponse, ExternalRequestResponse>> {
    return this.#doExternalConversation({
      conversation: data.conversation,
      prompt,
      selected: data.selected,
    });
  }

  async #handleExternalNetworkConversation(prompt: string, requestUrl: string):
      Promise<AsyncGenerator<ExternalRequestResponse, ExternalRequestResponse>> {
    const networkAgent = new NetworkAgent({
      aidaClient: this.#aidaClient,
      serverSideLoggingEnabled: isAiAssistanceServerSideLoggingEnabled(),
    });
    const request = await inspectNetworkRequestByUrl(requestUrl);
    if (!request) {
      return this.#generateErrorResponse(`Can't find request with the given selector ${requestUrl}`);
    }

    const calculator = new NetworkTimeCalculator.NetworkTransferTimeCalculator();
    calculator.updateBoundaries(request);

    return this.#createAndDoExternalConversation({
      conversationType: ConversationType.NETWORK,
      aiAgent: networkAgent,
      prompt,
      selected: new RequestContext(request, calculator),
    });
  }
}

export const enum ConversationHandlerEvents {
  EXTERNAL_REQUEST_RECEIVED = 'ExternalRequestReceived',
  EXTERNAL_CONVERSATION_STARTED = 'ExternalConversationStarted',
}

export interface EventTypes {
  [ConversationHandlerEvents.EXTERNAL_REQUEST_RECEIVED]: void;
  [ConversationHandlerEvents.EXTERNAL_CONVERSATION_STARTED]: ConversationType;
}
