/* eslint-disable import/no-cycle */
import { html, TemplateResult, CSSResult, nothing } from 'lit';
import { BroadcastCreatedEvent, BroadcastEvent, IFile, ISpace, uuidV4 } from '@api-client/core/build/browser.js';
import { ApplicationInit, ApplicationScreen } from '../ApplicationScreen.js';
import pageStyles from './ApiClient.styles.js';
import globalStyles from '../../styles/global-styles.js';
import typography from '../../styles/m3/typography.module.js';
import surface from '../../styles/m3/surface.module.js';
import dialogStyles from '../../styles/m3/dialog.module.js';
import { navigate } from '../../lib/route.js';
import { reactive } from '../../lib/decorators.js';
import { DataTable } from '../../ui/table/DataTable.js';
import { FilesPage } from './pages/Files.page.js';
import { SharedPage } from './pages/Shared.page.js';
import { TrashPage } from './pages/Trash.page.js';
import { SettingsPage } from './pages/Settings.page.js';
import type { IRoute, IRouteResult } from '../../mixins/RouteMixin.js';
import type Input from '../../ui/input/Input.js';
import type { HttpClientStoreBindings } from '../../bindings/base/HttpClientStoreBindings.js';
import type { NavigationBindings } from '../../bindings/base/NavigationBindings.js';
import type { FileBindings } from '../../bindings/base/FileBindings.js';
import { StoreBroadcast } from '../../http-client/store/StoreBroadcast.js';
import AppInfo from './AppInfo.js';
import '../../define/ui/ui-button.js';
import '../../define/app-navigation-item.js';
import '../../define/ui/ui-icon.js';
import '../../define/ui/ui-icon-button.js';
import '../../define/ui/ui-text-field.js';
import '../../define/ui/ui-dropdown-list.js';
import '../../define/ui/ui-divider.js';
import '../../define/http/certificate-add.js';

export interface PageInit extends ApplicationInit {
  idb: HttpClientStoreBindings;
  navigation: NavigationBindings;
  files: FileBindings;
}

/**
 * API Client is the entry point to the suite.
 * It renders the list of user files, allows to configure a connection to a store,
 * open user files in the corresponding application, or open
 * an independent application.
 */
export default class ApiClientScreen extends ApplicationScreen {
  static get routes(): IRoute[] {
    return [
      { pattern: '/files/(?<key>.*)', method: 'filesRoute', fallback: true, name: 'Files', title: 'Your files' },
      { pattern: '/shared', method: 'sharedRoute', name: 'Shared', title: 'Shared with you' },
      { pattern: '/shared/(?<key>.*)', method: 'sharedRoute', name: 'Shares', title: 'Shared with you' },
      { pattern: '/trash', method: 'trashRoute', name: 'Trash', title: 'Trash' },
      { pattern: '/settings', method: 'settingsRoute', name: 'Settings', title: 'Settings' },
      { pattern: '/certificates/add/?(?<parent>.*)', method: 'addCertRoute', name: 'Add certificate', title: 'Add certificate' },
    ];
  }

  static get styles(): CSSResult[] {
    return [globalStyles, pageStyles, typography, dialogStyles, DataTable.styles, surface];
  }

  /**
   * Whether the current user has spaces defined in the store.
   * When not, it runs the start screen configuring workspaces.
   */
  get hasSpaces(): boolean {
    const { spaces } = this;
    return !!spaces && !!spaces.length;
  }

  /**
   * @The list of user spaces.
   */
  protected spaces: ISpace[] = [];

  /**
   * The pagination cursor for workspaces.
   * As long there's a cursor, there are more spaces to request.
   */
  protected spacesCursor?: string;

  /**
   * The currently selected space.
   */
  @reactive() selectedSpace?: string;

  /**
   * The definition of the selected space.
   */
  get currentSpace(): ISpace | undefined {
    const { spaces, selectedSpace } = this;
    if (!spaces || !selectedSpace) {
      return undefined;
    }
    return spaces.find(i => i.key === selectedSpace);
  }

  protected files = new FilesPage(this);

  protected shared = new SharedPage(this);

  protected trash = new TrashPage(this);

  protected settings = new SettingsPage(this);

  spacesChannel = new BroadcastChannel(StoreBroadcast.spaces);

  trashChannel = new BroadcastChannel(StoreBroadcast.trash);

  constructor(init: PageInit) {
    super(init);
    this.bindings.idb = init.idb;
    this.bindings.navigation = init.navigation;
    this.bindings.files = init.files;
  }

  async initialize(): Promise<void> {
    if (!await this.isPlatformSupported()) {
      return;
    }
    await this.bindings.config.initialize();
    await this.initializeConfig();
    await this.bindings.navigation.initialize();
    await this.initializeStore();
    const key = await this.readSpaceKey();
    if (key) {
      this.bindings.store.space = key;
    }
    await this.bindings.idb.initialize();
    await this.bindings.store.initialize();
    await this.bindings.files.initialize();

    await this.setupWorkspace(key);

    await this.bindings.store.observeSpaces();
    if (key) {
      await this.bindings.store.observeFiles();
      await this.bindings.store.observeTrash();
    }
    await this.loadUser();
    this.spacesChannel.addEventListener('message', this.spacesMessageHandler.bind(this));
    this.trashChannel.addEventListener('message', this.trashMessageHandler.bind(this));
    this.initializeRouting();
    this.initialized = true;
  }

  reset(): void {
    this.spaces = [];
    this.spacesCursor = undefined;
    this.files.fs.resetList();
    this.shared.fs.resetList();
    this.trash.resetList();
  }

  override async storeChanged(): Promise<void> {
    this.page = undefined;
    this.initialized = false;
    await this.bindings.store.closeEventsSocket();
    this.reset();
    this.bindings.store.space = '';
    await this.initializeStore();
    const spaceKey = await this.readLastSpaceKey();
    if (spaceKey) {
      this.bindings.store.space = spaceKey;
    }
    if (!this.authenticated) {
      return;
    }
    try {
      await this.setupWorkspace(spaceKey);
      await this.bindings.store.observeSpaces();
      if (spaceKey) {
        await this.bindings.store.observeFiles();
        await this.bindings.store.observeTrash();
      }
      this.files.refresh();
      this.initialized = true;
    } catch (_) {
      // ...
    }
  }

  protected async setupWorkspace(key?: string): Promise<void> {
    await this.readSpaces();
    this.selectWorkspace(key);
  }

  protected async readSpaceKey(): Promise<string | undefined> {
    const url = new URL(window.location.href);
    const key = url.searchParams.get('space');
    if (key) {
      return key;
    }
    return this.readLastSpaceKey();
  }

  protected async createLastSpaceStoreKey(): Promise<string | undefined> {
    try {
      const env = await this.bindings.store.global.get();
      return `api-client.latest.space.${env.key}`;
    } catch (_) {
      return undefined;
    }
  }

  protected async readLastSpaceKey(): Promise<string | undefined> {
    const key = await this.createLastSpaceStoreKey();
    if (!key) {
      return undefined;
    }
    const value = await this.bindings.config.local.get(key);
    return value as string || undefined;
  }

  protected async deleteLastSpaceKey(): Promise<void> {
    const key = await this.createLastSpaceStoreKey();
    if (!key) {
      return;
    }
    await this.bindings.config.local.delete(key);
  }

  protected async storeLastSpaceKey(spaceKey: string): Promise<void> {
    const key = await this.createLastSpaceStoreKey();
    if (!key) {
      return;
    }
    await this.bindings.config.local.set(key, spaceKey);
  }

  selectWorkspace(key?: string): void {
    const { spaces } = this;
    if (!spaces.length) {
      this.selectedSpace = key;
      this.render();
      return;
    }
    if (key) {
      // check whether the key is in the spaces.
      const has = this.spaces.find(i => i.key === key);
      if (has) {
        this.selectedSpace = key;
      } else {
        this.selectedSpace = this.spaces[0].key;
      }
    } else {
      this.selectedSpace = this.spaces[0].key;
    }

    this.bindings.store.space = this.selectedSpace;

    this.render();
    this.files.fs.resetList();
    this.shared.fs.resetList();
    this.trash.resetList();
    if (this.page === 'files') {
      this.files.fs.debounceQuery();
      navigate('files'); // remove any folder info
    } else if (this.page === 'trash') {
      this.trash.readFiles();
    } else if (this.page === 'shared') {
      this.shared.fs.debounceQuery();
    }
    this.storeLastSpaceKey(this.selectedSpace);
  }

  async resetStoreObservers(): Promise<void> {
    await this.bindings.store.unobserveFiles();
    await this.bindings.store.unobserveTrash();
    if (this.selectedSpace) {
      await this.bindings.store.observeFiles();
      await this.bindings.store.observeTrash();
    }
  }

  protected async readSpaces(): Promise<void> {
    const spaces = await this.bindings.store.spaces.list({ limit: 100 });
    const { items, cursor } = spaces;
    this.spaces = items;
    this.spacesCursor = cursor;
  }

  protected trashMessageHandler(e: MessageEvent): void {
    const event = e.data as BroadcastEvent;
    if (event.space !== this.selectedSpace) {
      return;
    }
    this.trash.handleFileEvent(event);
  }

  protected spacesMessageHandler(e: MessageEvent): void {
    const event = e.data as BroadcastEvent;
    switch (event.operation) {
      case 'created': this.handleSpaceCreated((event as BroadcastCreatedEvent).data as ISpace); break;
      case 'deleted': this.handleSpaceDeleted(event.key as string); break;
      default:
    }
  }

  handleSpaceCreated(space: ISpace): void {
    if (!this.spaces) {
      this.spaces = [];
    }
    this.spaces.push(space);
    this.selectWorkspace(space.key);
    this.resetStoreObservers();
    this.render();
  }

  handleSpaceDeleted(key: string): void {
    if (!this.spaces) {
      this.spaces = [];
    }
    const index = this.spaces.findIndex(i => i.key === key);
    if (index === -1) {
      return;
    }
    this.spaces.splice(index, 1);
    this.render();
    if (this.selectedSpace === key) {
      this.deleteLastSpaceKey();
    }
    this.selectWorkspace();
    this.resetStoreObservers();
  }

  protected resetRoute(): void {
    this.page = undefined;
  }

  protected filesRoute(info: IRouteResult): void {
    this.resetRoute();
    this.page = 'files';
    const folder = info.params && info.params.key as string | undefined;
    const folderChanged = this.files.fs.parent !== folder;
    if (folderChanged) {
      this.files.fs.parent = folder;
      this.files.fs.resetList();
    }
    if (!this.files.fs.files.length || folderChanged) {
      this.files.fs.debounceQuery();
    }
  }

  protected sharedRoute(info: IRouteResult): void {
    this.resetRoute();
    this.page = 'shared';
    const folder = info.params && info.params.key as string | undefined;
    const folderChanged = this.shared.fs.parent !== folder;
    if (folderChanged) {
      this.shared.fs.parent = folder;
      this.shared.fs.resetList();
    }
    if (!this.shared.fs.files.length || folderChanged) {
      this.shared.fs.debounceQuery();
    }
  }

  protected trashRoute(): void {
    this.resetRoute();
    this.page = 'trash';
    if (!this.trash.files.length) {
      this.trash.readFiles();
    }
  }

  protected settingsRoute(): void {
    this.resetRoute();
    this.page = 'settings';
  }

  protected addCertRoute(): void {
    this.resetRoute();
    this.page = 'add-cert';
  }

  protected handleCertificateClose(): void {
    window.history.back();
  }

  protected mainNavHandler(e: Event): void {
    const target = e.target as HTMLElement;
    const { page } = target.dataset;
    if (!page) {
      return;
    }
    navigate(page);
  }

  protected workspaceCreateFormHandler(e: SubmitEvent): void {
    e.preventDefault();
    const form = e.target as HTMLFormElement;
    const input = form.querySelector('ui-text-field');
    if (!input) {
      return;
    }
    const { value } = input;
    this.addWorkspace(value);
  }

  protected async addWorkspace(name: string): Promise<void> {
    await this.bindings.store.spaces.add(name);
  }

  protected async deleteSpace(key: string): Promise<void> {
    this.freezeApp();
    await this.bindings.store.spaces.delete(key);
    this.unfreezeApp();
  }

  protected workspaceSelectHandler(e: CustomEvent): void {
    const item = e.detail.item as HTMLElement;
    const { key, action } = item.dataset;
    if (action === 'add') {
      this.openNewWorkspaceDialog();
    } else if (action === 'delete') {
      this.openDeleteWorkspaceDialog();
    } else if (key) {
      this.selectWorkspace(key);
      this.resetStoreObservers();
    }
  }

  openNewWorkspaceDialog(): void {
    this.openDialog('createWorkspaceDialog');
  }

  openDeleteWorkspaceDialog(): void {
    this.openDialog('deleteWorkspaceDialog');
  }

  cancelCreateWorkspaceDialog(): void {
    this.closeDialog('createWorkspaceDialog');
  }

  cancelDeleteWorkspaceDialog(): void {
    this.closeDialog('deleteWorkspaceDialog');
  }

  protected createWorkspaceHandler(e: SubmitEvent): void {
    const form = e.target as HTMLFormElement;
    const input = form.querySelector('ui-text-field');
    if (!input) {
      return;
    }
    const { submitter } = e;
    if (!submitter) {
      return;
    }
    const button = submitter as HTMLButtonElement; // or equivalent
    if (button.value !== 'submit') {
      return;
    }
    const { value } = input;
    input.value = '';
    this.addWorkspace(value);
  }

  protected async deleteWorkspaceHandler(): Promise<void> {
    this.cancelDeleteWorkspaceDialog();
    const { selectedSpace } = this;
    if (!selectedSpace) {
      return;
    }
    this.deleteSpace(selectedSpace);
  }

  closeDialog(id: string): void {
    const dialog = document.querySelector(`#${id}`) as HTMLDialogElement | null;
    if (!dialog) {
      return;
    }
    dialog.close();
  }

  openDialog(id: string, modal = true): void {
    const dialog = document.querySelector(`#${id}`) as HTMLDialogElement | null;
    if (!dialog) {
      return;
    }
    if (modal) {
      dialog.showModal();
    } else {
      dialog.show();
    }
  }

  freezeApp(): void {
    document.querySelector('#app')?.classList.add('inactive');
  }

  unfreezeApp(): void {
    document.querySelector('#app')?.classList.remove('inactive');
  }

  submitOnEnter(e: KeyboardEvent): void {
    //  || (!e.ctrlKey && !e.metaKey)
    if (!['Enter', 'NumEnter'].includes(e.code)) {
      return;
    }
    const input = e.target as Input;
    const { form } = input;
    if (!form) {
      return;
    }
    const button = form.querySelector('[submit]') as HTMLElement | null;
    if (button) {
      button.click();
    } else {
      form.submit();
    }
  }

  async renameFile(file: IFile, name: string): Promise<void> {
    if (!name) {
      return;
    }
    await this.bindings.store.files.patch(file.key, {
      app: AppInfo.code,
      appVersion: AppInfo.version,
      id: uuidV4(),
      patch: [
        {
          op: 'replace',
          path: '/info/name',
          value: name,
        }
      ],
    });
  }

  override pageTemplate(): TemplateResult {
    const { initialized, hasSpaces } = this;
    if (!initialized) {
      return super.pageTemplate();
    }
    if (!hasSpaces) {
      return this.spacesSetupTemplate();
    }
    return html`
      ${this.navigationTemplate()}
      ${this.headerTemplate()}
      ${this.mainTemplate()}
    `;
  }

  protected override navigationTemplate(): TemplateResult {
    const { page, isSingleUser } = this;
    return html`
    <nav aria-label="Main">
      <h1 class="title-small nav-title">API Client</h1>
      <ol class="nav-list">
        <li>
          <app-navigation-item data-page="files" .selected=${page === 'files'} @click="${this.mainNavHandler}">
            <ui-icon icon="collectionsBookmark" slot="icon"></ui-icon>
            Files
          </app-navigation-item>
        </li>
        ${isSingleUser ? nothing : html`
        <li>
          <app-navigation-item data-page="shared" .selected=${page === 'shared'} @click="${this.mainNavHandler}">
            <ui-icon icon="folderShared" slot="icon"></ui-icon>
            Shared with me
          </app-navigation-item>
        </li>
        `}
        <li>
          <app-navigation-item data-page="trash" .selected=${page === 'trash'} @click="${this.mainNavHandler}">
            <ui-icon icon="deleteFile" slot="icon"></ui-icon>
            Trash
          </app-navigation-item>
        </li>
        <li>
          <app-navigation-item data-page="settings" .selected=${page === 'settings'} @click="${this.mainNavHandler}">
            <ui-icon icon="settings" slot="icon"></ui-icon>
            Settings
          </app-navigation-item>
        </li>
      </ol>
    </nav>
    `;
  }

  protected override headerTemplate(): TemplateResult {
    const space = this.currentSpace;
    if (!space) {
      // this should not happen since we render different view in this scenario.
      return html`<header></header>`;
    }
    return html`
    <header>
      <div class="search-box">
        <ui-icon class="search-icon" icon="search"></ui-icon>
        <input type="text" aria-label="Search" placeholder="Search"/>
      </div>
      ${this.renderWorkspaceSelector(space)}
    </header>
    `;
  }

  protected renderWorkspaceSelector(space: ISpace): TemplateResult {
    const { spaces } = this;
    return html`
    <ui-dropdown-list class="spaced-right workspace-dropdown" horizontalAlign="right" @select="${this.workspaceSelectHandler}">
      <div class="workspace-selector" role="button">
        <div class="surface">
          <div class="content">
            <div class="body">
              <span class="workspace-label label-small">Workspace</span>
              <span class="workspace-selected body-large">${space.info.name || 'Unnamed space'}</span>
            </div>
            <div class="icon">
              <ui-icon icon="arrowDropDown"></ui-icon>
            </div>
          </div>
        </div>
      </div>

      <ui-list slot="dropdown" role="menu">
        ${spaces.map(s => html`<ui-list-item role="menuitem" data-key="${s.key}">${s.info.name}</ui-list-item>`)}
        <ui-divider role="presentation"></ui-divider>
        <ui-list-item image="icon" role="menuitem" data-action="add">
          <ui-icon icon="add" slot="start"></ui-icon>
          Add new
        </ui-list-item>
        <ui-list-item image="icon" role="menuitem" data-action="delete">
          <ui-icon icon="deleteFile" slot="start"></ui-icon>
          Delete workspace
        </ui-list-item>
      </ui-list>
    </ui-dropdown-list>
    `;
  }

  protected override mainTemplate(): TemplateResult {
    return html`
    <main class="body-medium">
      <div class="content surface1">
        ${this.renderPage()}
      </div>
    </main>
    ${this.renderCreateWorkspaceDialog()}
    ${this.renderDeleteWorkspaceDialog()}
    `;
  }

  protected renderPage(): TemplateResult {
    const { page ="files" } = this;
    switch (page) {
      case 'files': return this.files.render();
      case 'shared': return this.shared.render();
      case 'trash': return this.trash.render();
      case 'settings': return this.settings.render();
      case 'add-cert': return this.renderAddCertificate();
      default: return this.render404();
    }
  }

  protected renderModelsPage(): TemplateResult {
    return html`
    <section aria-label="Data models">
      <div class="section-header">
        <h2 class="headline-small">Data Models</h2>
        <ui-button class="right" type="outlined">
          <ui-icon icon="add" slot="icon"></ui-icon>
          New
        </ui-button>
        <ui-icon-button><ui-icon icon="moreVert"></ui-icon></ui-icon-button>
      </div>
    </section>`;
  }

  protected render404(): TemplateResult {
    return html`404`;
  }

  protected spacesSetupTemplate(): TemplateResult {
    return html`
    <section class="full-page-alert">
      <h1 class="display-large">Let's get you started</h1>
      <div class="workspace-dialog-info">
        <p class="body-large">
          The application uses <b>workspaces</b> to organize your work. You can switch context by changing selected workspace.
          Each workspace has an independent files list.
        </p>
        <form action="#" @submit="${this.workspaceCreateFormHandler}">
          <h2 class="title-medium">Create your first workspace</h2>
          <div class="workspace-intro-form">
            <ui-text-field class="input" label="Workspace name" name="name" value="Default" required invalidText="Workspace name is required"></ui-text-field>
            <ui-button type="filled" value="submit" submit>Create</ui-button>
          </div>
        </form>
      </div>
    </section>
    `;
  }

  protected renderCreateWorkspaceDialog(): TemplateResult {
    return html`
    <dialog id="createWorkspaceDialog">
      <form method="dialog" @submit="${this.createWorkspaceHandler}">
        <h3>New Workspace</h3>
        <div>
          <p class="body-medium">Enter name for the workspace</p>
          <ui-text-field class="input" label="Workspace name" name="name" required invalidText="Workspace name is required" @keydown="${this.submitOnEnter}"></ui-text-field>
        </div>
        <div class="dialog-buttons">
          <ui-button type="text" value="cancel" @click="${this.cancelCreateWorkspaceDialog}">Cancel</ui-button>
          <ui-button type="text" value="submit" submit>Create</ui-button>
        </div>
      </form>
    </dialog>
    `;
  }

  protected renderDeleteWorkspaceDialog(): TemplateResult {
    const { currentSpace } = this;
    const name = currentSpace && currentSpace.info.name || 'unnamed';
    return html`
    <dialog id="deleteWorkspaceDialog" class="warning">
      <h3>Delete Workspace?</h3>
      <div>
        <p class="body-medium">
          This will permanently delete all workspace <b>${name}</b>.
          The following data will be lost forever:
        </p>
        <ui-divider></ui-divider>
        <ul>
          <li>Files (projects, data models</li>
          <li>Trashed files</li>
        </ul>
        <ui-divider></ui-divider>
      </div>
      <div class="dialog-buttons">
        <ui-button type="text" value="cancel" @click="${this.cancelDeleteWorkspaceDialog}">Cancel</ui-button>
        <ui-button type="text" value="delete" @click="${this.deleteWorkspaceHandler}">Delete</ui-button>
      </div>
    </dialog>
    `;
  }

  protected renderAddCertificate(): TemplateResult {
    const { currentRoute, currentSpace } = this;
    const parent = currentRoute && currentRoute.params && currentRoute.params.parent as string | undefined;
    const space = currentSpace && currentSpace.key;
    return html`
    <h2 class="headline-small">Add Certificate</h2>
    <p class="body-medium">Import a certificate to use it in your HTTP projects. Note, imported certificate cannot be changed.</p>
    <certificate-add .folder="${parent}" .space="${space}" @close="${this.handleCertificateClose}"></certificate-add>
    `;
  }
}
