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

import type * as ProtocolProxyApi from '../../generated/protocol-proxy-api.js';
import type * as Protocol from '../../generated/protocol.js';
import type * as Common from '../common/common.js';

import {SDKModel} from './SDKModel.js';
import {Events as StorageKeyManagerEvents, StorageKeyManager} from './StorageKeyManager.js';
import {Capability, type Target} from './Target.js';

export class StorageBucketsModel extends SDKModel<EventTypes> implements ProtocolProxyApi.StorageDispatcher {
  private enabled = false;
  readonly storageAgent: ProtocolProxyApi.StorageApi;
  private readonly storageKeyManager: StorageKeyManager|null;
  private bucketsById = new Map<string, Protocol.Storage.StorageBucketInfo>();
  private trackedStorageKeys = new Set<string>();

  constructor(target: Target) {
    super(target);
    target.registerStorageDispatcher(this);
    this.storageAgent = target.storageAgent();
    this.storageKeyManager = target.model(StorageKeyManager);
  }

  getBuckets(): Set<Protocol.Storage.StorageBucketInfo> {
    return new Set(this.bucketsById.values());
  }

  getBucketsForStorageKey(storageKey: string): Set<Protocol.Storage.StorageBucketInfo> {
    const buckets = [...this.bucketsById.values()];
    return new Set(buckets.filter(({bucket}) => bucket.storageKey === storageKey));
  }

  getDefaultBucketForStorageKey(storageKey: string): Protocol.Storage.StorageBucketInfo|null {
    const buckets = [...this.bucketsById.values()];
    return buckets.find(({bucket}) => bucket.storageKey === storageKey && bucket.name === undefined) ?? null;
  }

  getBucketById(bucketId: string): Protocol.Storage.StorageBucketInfo|null {
    return this.bucketsById.get(bucketId) ?? null;
  }

  getBucketByName(storageKey: string, bucketName?: string): Protocol.Storage.StorageBucketInfo|null {
    if (!bucketName) {
      return this.getDefaultBucketForStorageKey(storageKey);
    }
    const buckets = [...this.bucketsById.values()];
    return buckets.find(({bucket}) => bucket.storageKey === storageKey && bucket.name === bucketName) ?? null;
  }

  deleteBucket(bucket: Protocol.Storage.StorageBucket): void {
    void this.storageAgent.invoke_deleteStorageBucket({bucket});
  }

  enable(): void {
    if (this.enabled) {
      return;
    }

    if (this.storageKeyManager) {
      this.storageKeyManager.addEventListener(StorageKeyManagerEvents.STORAGE_KEY_ADDED, this.storageKeyAdded, this);
      this.storageKeyManager.addEventListener(
          StorageKeyManagerEvents.STORAGE_KEY_REMOVED, this.storageKeyRemoved, this);
      for (const storageKey of this.storageKeyManager.storageKeys()) {
        this.addStorageKey(storageKey);
      }
    }

    this.enabled = true;
  }

  private storageKeyAdded(event: Common.EventTarget.EventTargetEvent<string>): void {
    this.addStorageKey(event.data);
  }
  private storageKeyRemoved(event: Common.EventTarget.EventTargetEvent<string>): void {
    this.removeStorageKey(event.data);
  }

  private addStorageKey(storageKey: string): void {
    if (this.trackedStorageKeys.has(storageKey)) {
      throw new Error('Can\'t call addStorageKey for a storage key if it has already been added.');
    }

    this.trackedStorageKeys.add(storageKey);
    void this.storageAgent.invoke_setStorageBucketTracking({storageKey, enable: true});
  }

  private removeStorageKey(storageKey: string): void {
    if (!this.trackedStorageKeys.has(storageKey)) {
      throw new Error('Can\'t call removeStorageKey for a storage key if it hasn\'t already been added.');
    }
    const bucketsForStorageKey = this.getBucketsForStorageKey(storageKey);
    for (const bucket of bucketsForStorageKey) {
      this.bucketRemoved(bucket);
    }
    this.trackedStorageKeys.delete(storageKey);
    void this.storageAgent.invoke_setStorageBucketTracking({storageKey, enable: false});
  }

  private bucketAdded(bucketInfo: Protocol.Storage.StorageBucketInfo): void {
    this.bucketsById.set(bucketInfo.id, bucketInfo);
    this.dispatchEventToListeners(Events.BUCKET_ADDED, {model: this, bucketInfo});
  }

  private bucketRemoved(bucketInfo: Protocol.Storage.StorageBucketInfo): void {
    this.bucketsById.delete(bucketInfo.id);
    this.dispatchEventToListeners(Events.BUCKET_REMOVED, {model: this, bucketInfo});
  }

  private bucketChanged(bucketInfo: Protocol.Storage.StorageBucketInfo): void {
    this.dispatchEventToListeners(Events.BUCKET_CHANGED, {model: this, bucketInfo});
  }

  private bucketInfosAreEqual(
      bucketInfo1: Protocol.Storage.StorageBucketInfo, bucketInfo2: Protocol.Storage.StorageBucketInfo): boolean {
    return bucketInfo1.bucket.storageKey === bucketInfo2.bucket.storageKey && bucketInfo1.id === bucketInfo2.id &&
        bucketInfo1.bucket.name === bucketInfo2.bucket.name && bucketInfo1.expiration === bucketInfo2.expiration &&
        bucketInfo1.quota === bucketInfo2.quota && bucketInfo1.persistent === bucketInfo2.persistent &&
        bucketInfo1.durability === bucketInfo2.durability;
  }

  storageBucketCreatedOrUpdated({bucketInfo}: Protocol.Storage.StorageBucketCreatedOrUpdatedEvent): void {
    const curBucket = this.getBucketById(bucketInfo.id);
    if (curBucket) {
      if (!this.bucketInfosAreEqual(curBucket, bucketInfo)) {
        this.bucketChanged(bucketInfo);
      }
    } else {
      this.bucketAdded(bucketInfo);
    }
  }

  storageBucketDeleted({bucketId}: Protocol.Storage.StorageBucketDeletedEvent): void {
    const curBucket = this.getBucketById(bucketId);
    if (curBucket) {
      this.bucketRemoved(curBucket);
    } else {
      throw new Error(
          `Received an event that Storage Bucket '${bucketId}' was deleted, but it wasn't in the StorageBucketsModel.`);
    }
  }

  interestGroupAccessed(_event: Protocol.Storage.InterestGroupAccessedEvent): void {
  }

  interestGroupAuctionEventOccurred(_event: Protocol.Storage.InterestGroupAuctionEventOccurredEvent): void {
  }

  interestGroupAuctionNetworkRequestCreated(_event: Protocol.Storage.InterestGroupAuctionNetworkRequestCreatedEvent):
      void {
  }

  indexedDBListUpdated(_event: Protocol.Storage.IndexedDBListUpdatedEvent): void {
  }

  indexedDBContentUpdated(_event: Protocol.Storage.IndexedDBContentUpdatedEvent): void {
  }

  cacheStorageListUpdated(_event: Protocol.Storage.CacheStorageListUpdatedEvent): void {
  }

  cacheStorageContentUpdated(_event: Protocol.Storage.CacheStorageContentUpdatedEvent): void {
  }

  sharedStorageAccessed(_event: Protocol.Storage.SharedStorageAccessedEvent): void {
  }

  sharedStorageWorkletOperationExecutionFinished(
      _event: Protocol.Storage.SharedStorageWorkletOperationExecutionFinishedEvent): void {
  }
}

SDKModel.register(StorageBucketsModel, {capabilities: Capability.STORAGE, autostart: false});

export const enum Events {
  BUCKET_ADDED = 'BucketAdded',
  BUCKET_REMOVED = 'BucketRemoved',
  BUCKET_CHANGED = 'BucketChanged',
}

export interface BucketEvent {
  model: StorageBucketsModel;
  bucketInfo: Protocol.Storage.StorageBucketInfo;
}

export interface EventTypes {
  [Events.BUCKET_ADDED]: BucketEvent;
  [Events.BUCKET_REMOVED]: BucketEvent;
  [Events.BUCKET_CHANGED]: BucketEvent;
}
