// 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.

import type * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import type * as Platform from '../../core/platform/platform.js';
import {createIcon} from '../../ui/kit/kit.js';
import * as UI from '../../ui/legacy/legacy.js';

import {ApplicationPanelTreeElement} from './ApplicationPanelTreeElement.js';
import {
  DeviceBoundSessionModelEvents,
  type DeviceBoundSessionModelEventTypes,
  type DeviceBoundSessionsModel
} from './DeviceBoundSessionsModel.js';
import type {ResourcesPanel} from './ResourcesPanel.js';

const UIStrings = {
  /**
   *@description Text for section title Application panel sidebar. A website
   * may decide to create a session for a user, for example when the user logs
   * in. They can use a protocol to make it a "device bound session". That
   * means that when the session expires, it is only possible for it to be
   * extended on the device it was created on. Thus the session is considered
   * to be bound to that device. For more details on the protocol, see
   * https://github.com/w3c/webappsec-dbsc/blob/main/README.md and
   * https://w3c.github.io/webappsec-dbsc/.
   */
  deviceBoundSessions: 'Device bound sessions',
  /**
   *@description Empty state description for root tree element and site tree
   * elements. A website may decide to create a session for a user, for example
   * when the user logs in. They can use a protocol to make it a "device bound
   * session". That means that when the session expires, it is only possible
   * for it to be extended on the device it was created on. Thus the session
   * is considered to be bound to that device. A session can have various events,
   * such as when it's first created, when it's extended, or when it's
   * terminated. For more details on the protocol, see
   * https://github.com/w3c/webappsec-dbsc/blob/main/README.md and
   * https://w3c.github.io/webappsec-dbsc/.
   */
  deviceBoundSessionsCategoryDescription: 'On this page you can view device bound sessions and associated events',
  /**
   *@description Events are sometimes linked to sessions. These are grouped
   * visually either by session name or by 'No session' if any events are not
   * linked to a session.
   */
  noSession: 'No session',
  /**
   *@description Tooltip text for a terminated session.
   *@example {session_1} sessionName
   */
  terminatedSession: '{sessionName}, Session terminated',
  /**
   *@description Tooltip text for a session with errors.
   *@example {session_1} sessionName
   */
  sessionWithErrors: '{sessionName}, Session has errors',
} as const;

const str_ = i18n.i18n.registerUIStrings('panels/application/DeviceBoundSessionsTreeElement.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

export class RootTreeElement extends ApplicationPanelTreeElement {
  #model: DeviceBoundSessionsModel;
  #sites = new Map<string, {
    siteTreeElement: ApplicationPanelTreeElement,
    sessions: Map<string|undefined, ApplicationPanelTreeElement>,
  }>();

  constructor(storagePanel: ResourcesPanel, model: DeviceBoundSessionsModel) {
    super(storagePanel, i18nString(UIStrings.deviceBoundSessions), /* expandable=*/ true, 'device-bound-sessions-root');
    this.setLeadingIcons([createIcon('lock-person')]);
    this.#model = model;
  }

  override get itemURL(): Platform.DevToolsPath.UrlString {
    return 'device-bound-sessions://' as Platform.DevToolsPath.UrlString;
  }

  override onselect(selectedByUser?: boolean): boolean {
    super.onselect(selectedByUser);
    this.resourcesPanel.showDeviceBoundSessionDefault(
        this.#model, i18nString(UIStrings.deviceBoundSessions),
        i18nString(UIStrings.deviceBoundSessionsCategoryDescription));
    return false;
  }

  override onbind(): void {
    super.onbind();
    this.#model.addEventListener(DeviceBoundSessionModelEvents.INITIALIZE_SESSIONS, this.#onNewSessions, this);
    this.#model.addEventListener(DeviceBoundSessionModelEvents.ADD_VISIBLE_SITE, this.#onVisibleSiteAdded, this);
    this.#model.addEventListener(DeviceBoundSessionModelEvents.CLEAR_VISIBLE_SITES, this.#onVisibleSitesCleared, this);
    this.#model.addEventListener(DeviceBoundSessionModelEvents.EVENT_OCCURRED, this.#onEventOccurred, this);
    this.#model.addEventListener(DeviceBoundSessionModelEvents.CLEAR_EVENTS, this.#onClearEvents, this);
  }

  override onunbind(): void {
    super.onunbind();
    this.#model.removeEventListener(DeviceBoundSessionModelEvents.INITIALIZE_SESSIONS, this.#onNewSessions, this);
    this.#model.removeEventListener(DeviceBoundSessionModelEvents.ADD_VISIBLE_SITE, this.#onVisibleSiteAdded, this);
    this.#model.removeEventListener(
        DeviceBoundSessionModelEvents.CLEAR_VISIBLE_SITES, this.#onVisibleSitesCleared, this);
    this.#model.removeEventListener(DeviceBoundSessionModelEvents.EVENT_OCCURRED, this.#onEventOccurred, this);
    this.#model.removeEventListener(DeviceBoundSessionModelEvents.CLEAR_EVENTS, this.#onClearEvents, this);
  }

  #updateSiteTreeElementVisibility(site: string): void {
    const siteMapEntry = this.#sites.get(site);
    if (!siteMapEntry) {
      return;
    }

    const siteTreeElement = siteMapEntry.siteTreeElement;
    const isElementPresent = this.indexOfChild(siteTreeElement) >= 0;
    const isSiteAllowed = this.#model.isSiteVisible(site);

    if (isSiteAllowed && !isElementPresent) {
      // Appending a child element that already has children requires a workaround of
      // detaching and repopulating them so that the selection background color UI works.
      // TODO(crbug.com/486866983): Can this fix safely be moved to Treeoutline.ts?
      this.appendChild(siteTreeElement);
      const children = [...siteTreeElement.children()];
      siteTreeElement.removeChildren();
      for (const child of children) {
        siteTreeElement.appendChild(child);
      }
    } else if (!isSiteAllowed && isElementPresent) {
      this.removeChild(siteTreeElement);
    }
  }

  #updateElementIconAndStyling(
      sessionElement: ApplicationPanelTreeElement, isSessionTerminated: boolean, sessionHasErrors: boolean): void {
    const title = sessionElement.title as string;
    if (isSessionTerminated) {
      sessionElement.listItemElement.classList.add('device-bound-session-terminated');
      sessionElement.setLeadingIcons([createIcon('database-off')]);
      const terminatedTitle = i18nString(UIStrings.terminatedSession, {sessionName: title});
      UI.ARIAUtils.setLabel(sessionElement.listItemElement, terminatedTitle);
      return;
    }
    sessionElement.listItemElement.classList.remove('device-bound-session-terminated');
    if (sessionHasErrors) {
      sessionElement.setLeadingIcons([createIcon('warning')]);
      const errorTitle = i18nString(UIStrings.sessionWithErrors, {sessionName: title});
      UI.ARIAUtils.setLabel(sessionElement.listItemElement, errorTitle);
    } else {
      sessionElement.setLeadingIcons([createIcon('database')]);
      UI.ARIAUtils.setLabel(sessionElement.listItemElement, title);
    }
  }

  #updateIconAndStyling(site: string, sessionId: string|undefined): void {
    const isSessionTerminated = this.#model.isSessionTerminated(site, sessionId);
    const sessionHasErrors = this.#model.sessionHasErrors(site, sessionId);
    const siteMapEntry = this.#sites.get(site);
    if (!siteMapEntry) {
      return;
    }
    const sessionElement = siteMapEntry.sessions.get(sessionId);
    if (!sessionElement) {
      return;
    }
    this.#updateElementIconAndStyling(sessionElement, isSessionTerminated, sessionHasErrors);
  }

  #removeWarningIcons(noLongerFailedSessions: Map<string, Array<string|undefined>>): void {
    for (const [site, noLongerFailedSessionIds] of noLongerFailedSessions) {
      const siteData = this.#sites.get(site);
      if (siteData) {
        for (const noLongerFailedSessionId of noLongerFailedSessionIds) {
          const sessionElement = siteData.sessions.get(noLongerFailedSessionId);
          if (sessionElement) {
            const isSessionTerminated = this.#model.isSessionTerminated(site, noLongerFailedSessionId);
            this.#updateElementIconAndStyling(sessionElement, isSessionTerminated, /* sessionHasErrors=*/ false);
          }
        }
      }
    }
  }

  #addSiteSessionIfMissing(site: string, sessionId: string|undefined): void {
    let siteMapEntry = this.#sites.get(site);
    if (!siteMapEntry) {
      const siteElement = new ApplicationPanelTreeElement(
          this.resourcesPanel, site, /* expandable=*/ true, 'device-bound-sessions-site');
      siteElement.setLeadingIcons([createIcon('cloud')]);
      siteElement.itemURL = `device-bound-sessions://${site}` as Platform.DevToolsPath.UrlString;

      const defaultOnSelect = siteElement.onselect.bind(siteElement);
      siteElement.onselect = (selectedByUser?: boolean): boolean => {
        defaultOnSelect(selectedByUser);
        this.resourcesPanel.showDeviceBoundSessionDefault(
            this.#model, i18nString(UIStrings.deviceBoundSessions),
            i18nString(UIStrings.deviceBoundSessionsCategoryDescription));
        return false;
      };

      siteMapEntry = {siteTreeElement: siteElement, sessions: new Map()};
      this.#sites.set(site, siteMapEntry);
    }

    if (!siteMapEntry.sessions.has(sessionId)) {
      const sessionElement = new ApplicationPanelTreeElement(
          this.resourcesPanel, sessionId ?? i18nString(UIStrings.noSession), false, 'device-bound-sessions-session');
      if (sessionId === undefined) {
        sessionElement.listItemElement.classList.add('no-device-bound-session');
      }
      sessionElement.setLeadingIcons([createIcon('database')]);
      sessionElement.itemURL = `device-bound-sessions://${site}/${sessionId || ''}` as Platform.DevToolsPath.UrlString;
      const defaultOnSelect = sessionElement.onselect.bind(sessionElement);
      sessionElement.onselect = (selectedByUser?: boolean): boolean => {
        defaultOnSelect(selectedByUser);
        this.resourcesPanel.showDeviceBoundSession(this.#model, site, sessionId);
        return false;
      };

      if (sessionId === undefined) {
        // The "No session" session is always listed at the top.
        siteMapEntry.siteTreeElement.insertChild(sessionElement, 0);
      } else {
        siteMapEntry.siteTreeElement.appendChild(sessionElement);
      }
      siteMapEntry.sessions.set(sessionId, sessionElement);
    }

    this.#updateSiteTreeElementVisibility(site);
  }

  #removeEmptyElements(emptySessions: Map<string, Array<string|undefined>>, emptySites: Set<string>): void {
    for (const emptySite of emptySites) {
      const siteData = this.#sites.get(emptySite);
      if (siteData) {
        this.removeChild(siteData.siteTreeElement);
        this.#sites.delete(emptySite);
      }
    }

    for (const [site, emptySessionIds] of emptySessions) {
      const siteData = this.#sites.get(site);
      if (siteData) {
        for (const emptySessionId of emptySessionIds) {
          const sessionElement = siteData.sessions.get(emptySessionId);
          if (sessionElement) {
            siteData.siteTreeElement.removeChild(sessionElement);
            siteData.sessions.delete(emptySessionId);
          }
        }
      }
    }
  }

  #onNewSessions({data: {sessions}}: Common.EventTarget.EventTargetEvent<
                 DeviceBoundSessionModelEventTypes[DeviceBoundSessionModelEvents.INITIALIZE_SESSIONS]>): void {
    for (const session of sessions) {
      this.#addSiteSessionIfMissing(session.key.site, session.key.id);
    }
  }

  #onVisibleSiteAdded(
      {data: {site}}: Common.EventTarget
          .EventTargetEvent<DeviceBoundSessionModelEventTypes[DeviceBoundSessionModelEvents.ADD_VISIBLE_SITE]>): void {
    this.#updateSiteTreeElementVisibility(site);
  }

  #onVisibleSitesCleared(): void {
    this.removeChildren();
  }

  #onEventOccurred(
      {data: {site, sessionId}}: Common.EventTarget
          .EventTargetEvent<DeviceBoundSessionModelEventTypes[DeviceBoundSessionModelEvents.EVENT_OCCURRED]>): void {
    this.#addSiteSessionIfMissing(site, sessionId);
    this.#updateIconAndStyling(site, sessionId);
  }

  #onClearEvents({data: {emptySessions, emptySites, noLongerFailedSessions}}: Common.EventTarget
                     .EventTargetEvent<DeviceBoundSessionModelEventTypes[DeviceBoundSessionModelEvents.CLEAR_EVENTS]>):
      void {
    this.#removeEmptyElements(emptySessions, emptySites);
    this.#removeWarningIcons(noLongerFailedSessions);
  }
}
