// 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 type * as SDK from '../../core/sdk/sdk.js';
import * as GreenDev from '../greendev/greendev.js';

import {AnnotationType} from './AnnotationType.js';

export interface BaseAnnotationData {
  id: number;
  type: AnnotationType;
  message: string;
  // Sometimes the anchor for an annotation is not known, but is provided using a
  // string id instead (which can be converted to a proper `anchor`).
  lookupId: string;
  // Sometimes we want annotations to anchor to a particular string on the page.
  anchorToString?: string;
}

export interface ElementsAnnotationData extends BaseAnnotationData {
  type: AnnotationType.ELEMENT_NODE;
  anchor?: SDK.DOMModel.DOMNode;
}

export interface NetworkRequestAnnotationData extends BaseAnnotationData {
  type: AnnotationType.NETWORK_REQUEST;
  anchor?: SDK.NetworkRequest.NetworkRequest;
}

export interface NetworkRequestDetailsAnnotationData extends BaseAnnotationData {
  type: AnnotationType.NETWORK_REQUEST_SUBPANEL_HEADERS;
  anchor?: SDK.NetworkRequest.NetworkRequest;
}

export const enum Events {
  ANNOTATION_ADDED = 'AnnotationAdded',
  ANNOTATION_DELETED = 'AnnotationDeleted',
  ALL_ANNOTATIONS_DELETED = 'AllAnnotationsDeleted',
}

export interface EventTypes {
  [Events.ANNOTATION_ADDED]: BaseAnnotationData;
  [Events.ANNOTATION_DELETED]: {id: number};
  [Events.ALL_ANNOTATIONS_DELETED]: void;
}

export class AnnotationRepository {
  static #instance: AnnotationRepository|null = null;
  static #hasRepliedGreenDevDisabled = false;
  static #hasShownFlagWarning = false;

  #events = new Common.ObjectWrapper.ObjectWrapper<EventTypes>();
  #annotationData: BaseAnnotationData[] = [];
  #nextId = 0;

  static instance(): AnnotationRepository {
    if (!AnnotationRepository.#instance) {
      AnnotationRepository.#instance = new AnnotationRepository();
    }
    return AnnotationRepository.#instance;
  }

  static annotationsEnabled(): boolean {
    const enabled = GreenDev.Prototypes.instance().isEnabled('aiAnnotations');
    // TODO(finnur): Fix race when Repository is created before feature flags have been set properly.
    if (!enabled) {
      this.#hasRepliedGreenDevDisabled = true;
    } else if (this.#hasRepliedGreenDevDisabled && !this.#hasShownFlagWarning) {
      console.warn(
          'Flag controlling GreenDev has flipped from false to true. ' +
          'Only some callers will expect GreenDev to be enabled, which can lead to unexpected results.');
      this.#hasShownFlagWarning = true;
    }
    return Boolean(enabled);
  }

  addEventListener<T extends keyof EventTypes>(
      eventType: T, listener: (arg0: Common.EventTarget.EventTargetEvent<EventTypes[T]>) => void,
      thisObject?: Object): Common.EventTarget.EventDescriptor<EventTypes, T> {
    if (!AnnotationRepository.annotationsEnabled()) {
      console.warn('Received request to add event listener with annotations disabled');
    }
    return this.#events.addEventListener(eventType, listener, thisObject);
  }

  getAnnotationDataByType(type: AnnotationType): BaseAnnotationData[] {
    if (!AnnotationRepository.annotationsEnabled()) {
      console.warn('Received query for annotation types with annotations disabled');
      return [];
    }

    const annotations = this.#annotationData.filter(annotation => annotation.type === type);
    return annotations;
  }

  getAnnotationDataById(id: number): BaseAnnotationData|undefined {
    if (!AnnotationRepository.annotationsEnabled()) {
      console.warn('Received query for annotation type with annotations disabled');
      return undefined;
    }

    return this.#annotationData.find(annotation => annotation.id === id);
  }

  #getExistingAnnotation(type: AnnotationType, anchor?: SDK.DOMModel.DOMNode|SDK.NetworkRequest.NetworkRequest|string):
      BaseAnnotationData|undefined {
    const annotations = this.getAnnotationDataByType(type);
    const annotation = annotations.find(annotation => {
      if (typeof anchor === 'string') {
        return annotation.lookupId === anchor;
      }
      switch (type) {
        case AnnotationType.ELEMENT_NODE: {
          const elementAnnotation = annotation as ElementsAnnotationData;
          return elementAnnotation.anchor === anchor;
        }
        case AnnotationType.NETWORK_REQUEST_SUBPANEL_HEADERS: {
          const networkRequestDetailsAnnotation = annotation as NetworkRequestDetailsAnnotationData;
          return networkRequestDetailsAnnotation.anchor === anchor;
        }
        default:
          console.warn('[AnnotationRepository] Unknown AnnotationType', type);
          return false;
      }
    });
    return annotation;
  }

  #updateExistingAnnotationLabel(
      label: string, type: AnnotationType,
      anchor?: SDK.DOMModel.DOMNode|SDK.NetworkRequest.NetworkRequest|string): boolean {
    const annotation = this.#getExistingAnnotation(type, anchor);
    if (annotation) {
      // TODO(finnur): This should work for annotations that have not been displayed yet,
      // but we need to also notify the AnnotationManager for those that have been shown.
      annotation.message = label;
      return true;
    }
    return false;
  }

  addElementsAnnotation(
      label: string,
      anchor?: SDK.DOMModel.DOMNode|string,
      anchorToString?: string,
      ): void {
    if (!AnnotationRepository.annotationsEnabled()) {
      console.warn('Received annotation registration with annotations disabled');
      return;
    }

    if (this.#updateExistingAnnotationLabel(label, AnnotationType.ELEMENT_NODE, anchor)) {
      return;
    }

    const annotationData: ElementsAnnotationData = {
      id: this.#nextId++,
      type: AnnotationType.ELEMENT_NODE,
      message: label,
      lookupId: typeof anchor === 'string' ? anchor : '',
      anchor: typeof anchor !== 'string' ? anchor : undefined,
      anchorToString,
    };
    this.#annotationData.push(annotationData);
    // eslint-disable-next-line no-console
    console.log('[AnnotationRepository] Added element annotation:', label, {
      annotationData,
      annotations: this.#annotationData.length,
    });
    this.#events.dispatchEventToListeners(Events.ANNOTATION_ADDED, annotationData);
  }

  addNetworkRequestAnnotation(
      label: string,
      anchor?: SDK.NetworkRequest.NetworkRequest|string,
      anchorToString?: string,
      ): void {
    if (!AnnotationRepository.annotationsEnabled()) {
      console.warn('Received annotation registration with annotations disabled');
      return;
    }

    // We only need to update the NETWORK_REQUEST_SUBPANEL_HEADERS because the
    // NETWORK_REQUEST Annotation has no meaningful label.
    if (this.#updateExistingAnnotationLabel(label, AnnotationType.NETWORK_REQUEST_SUBPANEL_HEADERS, anchor)) {
      return;
    }

    const annotationData: NetworkRequestAnnotationData = {
      id: this.#nextId++,
      type: AnnotationType.NETWORK_REQUEST,
      message: '',
      lookupId: typeof anchor === 'string' ? anchor : '',
      anchor: typeof anchor !== 'string' ? anchor : undefined,
      anchorToString,
    };
    this.#annotationData.push(annotationData);
    // eslint-disable-next-line no-console
    console.log('[AnnotationRepository] Added annotation:', label, {
      annotationData,
      annotations: this.#annotationData.length,
    });
    this.#events.dispatchEventToListeners(Events.ANNOTATION_ADDED, annotationData);

    const annotationDetailsData: NetworkRequestDetailsAnnotationData = {
      id: this.#nextId++,
      type: AnnotationType.NETWORK_REQUEST_SUBPANEL_HEADERS,
      message: label,
      lookupId: typeof anchor === 'string' ? anchor : '',
      anchor: typeof anchor !== 'string' ? anchor : undefined,
      anchorToString,
    };

    this.#annotationData.push(annotationDetailsData);
    this.#events.dispatchEventToListeners(Events.ANNOTATION_ADDED, annotationDetailsData);
  }

  deleteAllAnnotations(): void {
    this.#annotationData = [];
    this.#events.dispatchEventToListeners(Events.ALL_ANNOTATIONS_DELETED);
    // eslint-disable-next-line no-console
    console.log('[AnnotationRepository] Deleting all annotations');
  }

  deleteAnnotation(id: number): void {
    const index = this.#annotationData.findIndex(annotation => annotation.id === id);
    if (index === -1) {
      console.warn(`[AnnotationRepository] Could not find annotation with id ${id}`);
      return;
    }
    this.#annotationData.splice(index, 1);
    this.#events.dispatchEventToListeners(Events.ANNOTATION_DELETED, {id});
    // eslint-disable-next-line no-console
    console.log(`[AnnotationRepository] Deleted annotation with id ${id}`);
  }
}
