import { html, TemplateResult, CSSResult } from 'lit';
import { 
  DataNamespace, IDataNamespace, ApiError, Events as CoreEvents, 
  DataNamespaceKind, IPatchRevision, DataEntityKind, DataModelKind, 
  IApplication,
  BroadcastEvent,
  FilePatchBroadcastEvent,
  IPatchInfo,
} from '@api-client/core/build/browser.js';
import { JsonPatch, Patch } from '@api-client/json';
import { ApplicationInit, ApplicationScreen } from '../ApplicationScreen.js';
import { query, reactive } from '../../lib/decorators.js';
import { IRoute, IRouteResult } from '../../mixins/RouteMixin.js';
import { navigate } from '../../lib/route.js';
import { SchemaDesignerContextMenu } from './SchemaDesignerContextMenu.js';
import NavElement from '../../elements/schema-design/SchemaDesignNavigation.js';
import AppInfo from './AppInfo.js';
import { randomString } from '../../lib/Random.js';
import { ISelectDetail } from '../../elements/navigation/AppNavigationElement.js';
import pageStyles from './SchemaDesigner.styles.js';
import globalStyles from '../../styles/global-styles.js';
import typography from '../../styles/m3/typography.module.js';
import dialogStyles from '../../styles/m3/dialog.module.js';
import { StoreBroadcast } from '../../http-client/store/StoreBroadcast.js';
import { EventTypes } from '../../events/EventTypes.js';
import { Events } from '../../events/Events.js';
import type { HttpClientStoreBindings } from '../../bindings/base/HttpClientStoreBindings.js';
import '../../define/schema-design/schema-design-navigation.js';
import '../../define/data/data-entity-editor.js';
import '../../define/data/data-model-visualization.js';
import '../../define/ui/ui-button.js';

export interface PageInit extends ApplicationInit {
  idb: HttpClientStoreBindings;
}

export default class SchemaDesignerScreen extends ApplicationScreen {
  static get styles(): CSSResult[] {
    // return [styles, globalStyles, mainLayout];
    return [globalStyles, pageStyles, typography, dialogStyles];
  }

  static get routes(): IRoute[] {
    return [
      { pattern: '/default', method: 'defaultRoute', fallback: true, name: 'Schema Designer', title: 'Schema Designer Home home' },
      { pattern: '/404', method: 'e404Route', name: 'Not found', title: 'Not found' },
      // { pattern: '/namespace/(?<key>.*)', method: 'namespaceRoute', name: 'Namespace', title: 'A namespace' },
      { pattern: '/model/(?<key>.*)', method: 'modelRoute', name: 'Data model', title: 'A data model' },
      { pattern: '/entity/(?<key>.*)', method: 'entityRoute', name: 'Entity model', title: 'An entity model' },
    ];
  }

  protected _key = '';
  
  /**
   * The schema file key.
   */
  get key(): string {
    return this._key;
  }

  /**
   * The read data namespace.
   */
  @reactive() root?: DataNamespace;

  protected menu: SchemaDesignerContextMenu;

  @query('schema-design-navigation') nav?: NavElement;

  protected updatingSchema?: boolean;

  protected isDirty?: boolean;

  protected schema?: string;

  /**
   * Own patches sent to the server.
   * These are removed after the sever event is received.
   */
  protected pendingPatches = new Map<string, JsonPatch>();

  /**
   * The list of patches made by the current user and received from the server since the beginning of this session
   * or page reload.
   * This is to used to recognize own patches so it won't process a patch that is already applied to the project.
   */
  protected livePatches = new Map<string, IPatchRevision>();

  /**
   * The key of the selected item in the root namespace. It corresponds to the `page`.
   */
  @reactive() selected?: string;

  mediaChannel = new BroadcastChannel(StoreBroadcast.media);

  protected _workspace = '';

  /**
   * The key of the workspace that has the project.
   */
  get workspace(): string {
    return this._workspace;
  }

  constructor(init: PageInit) {
    super(init);
    this.bindings.idb = init.idb;
    this.menu = new SchemaDesignerContextMenu();
    this.menu.connect();
    this.menu.store.set('callback', this.contextMenuMutationCallback.bind(this));
  }

  override async initialize(): Promise<void> {
    if (!await this.isPlatformSupported()) {
      return;
    }
    try {
      this.setupWorkspaceParameters();
    } catch (e) {
      this.initialized = true;
      this.reportCriticalError((e as Error).message);
      return;
    }
    await this.bindings.config.initialize();
    await this.initializeConfig();

    await this.bindings.store.initialize();

    await this.initializeStore();
    await this.loadUser();

    this.bindings.store.space = this.workspace;

    await this.bindings.idb.initialize();

    const hasFile = await this.setupNamespace(this.key);
    if (!hasFile) {
      this.initializeRouting();
      this.initialized = true;
      return;
    }
    this.mediaChannel.addEventListener('message', this.mediaMessageHandler.bind(this))
    this.initializeRouting();
    this.initialized = true;
    window.addEventListener(EventTypes.SchemaDesign.changed, this.contextMenuMutationCallback.bind(this));
  }

  override async storeChanged(): Promise<void> {
    // ...
  }

  protected setupWorkspaceParameters(): void {
    const url = new URL(window.location.href);
    const key = url.searchParams.get('key');
    const space = url.searchParams.get('space');
    if (key) {
      this._key = key;
    } else {
      throw new Error(`The project key is not set. Go back to the start page.`);
    }
    if (space) {
      this._workspace = space;
    } else {
      throw new Error(`The space key is not set. Go back to the start page.`);
    }
    this.bindings.store.space = space;
  }

  /**
   * Used to read the namespace file from the backend.
   */
  async readNamespaceFile(key: string): Promise<IDataNamespace> {
    const media = await this.bindings.store.media.read(key) as IDataNamespace;
    return media;
  }

  /**
   * Stores the patch in the data store.
   * @param key The key of the media file.
   * @param id The generated id of the patch
   * @param value The patch to apply in the backend
   * @param app The application info.
   */
  async storePatch(key: string, id: string, value: JsonPatch, app: IApplication): Promise<void> {
    const info: IPatchInfo = {
      app: app.code,
      appVersion: app.version,
      patch: value,
      id,
    };
    await this.bindings.store.media.patch(key, info);
  }

  /**
   * A handler for the broadcast channel message which informs about changes to files media.
   */
  protected mediaMessageHandler(e: MessageEvent): void {
    const event = e.data as BroadcastEvent;
    if (event.space !== this.workspace) {
      return;
    }
    this.processChangeFileEvent(event);
  }

  protected openStart(): void {
    Events.Navigation.App.runHttpClient({ space: this.workspace });
  }

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

  protected defaultRoute(): void {
    this.resetRoute();
    this.page = 'default';
  }

  protected e404Route(): void {
    this.resetRoute();
    this.page = '404';
  }

  // protected namespaceRoute(info: IRouteResult): void {
  //   if (!info.params || !info.params.key) {
  //     throw new Error(`Invalid route configuration. Missing parameters.`);
  //   }
  //   this.resetRoute();
  //   const key = info.params.key as string;
  //   this.page = 'namespace';
  //   this.selected = key;
  //   const { root } = this;
  //   if (!root) {
  //     return;
  //   }
  //   this.current = root.findNamespace(key);
  // }

  protected async modelRoute(info: IRouteResult): Promise<void> {
    if (!info.params || !info.params.key) {
      throw new Error(`Invalid route configuration. Missing parameters.`);
    }
    this.resetRoute();
    const key = info.params.key as string;
    this.page = 'model';
    this.selected = key;
  }

  protected entityRoute(info: IRouteResult): void {
    if (!info.params || !info.params.key) {
      throw new Error(`Invalid route configuration. Missing parameters.`);
    }
    this.resetRoute();
    const key = info.params.key as string;
    this.page = 'entity';
    this.selected = key;
  }

  /**
   * @returns The value of the meta file key encoded in the query params under the `key`.
   */
  protected readFileKey(): string | undefined {
    const url = new URL(window.location.href);
    const key = url.searchParams.get('key');
    return key || undefined;
  }

  /**
   * Setups the namespace file during app initialization.
   * 
   * @param key The namespace file id
   * @returns true when the namespace was setup correctly. False when there was an error.
   */
  protected async setupNamespace(key: string): Promise<boolean> {
    try {
      const media = await this.readNamespaceFile(key);
      this.schema = JSON.stringify(media);
      this.root = new DataNamespace(media);
      this.menu.store.set('schema', this.root);
    } catch (e) {
      const cause = e as ApiError;
      if (cause.code === 404) {
        navigate('404');
        return false;
      }
      CoreEvents.Telemetry.exception(cause.message, true);
      this.reportCriticalError(cause.message);
    }
    return true;
  }

  /**
   * A reference to this function is passed to the context menu
   * and it is called when a project mutates.
   */
  protected contextMenuMutationCallback(): void {
    this.render();
    this.updateSchema();
    const { nav } = this;
    if (nav) {
      nav.requestUpdate();
    }
  }

  /**
   * Apples changes to a DataNamespace file.
   * @param event The backend event received from the store.
   */
  protected processChangeFileEvent(event: BroadcastEvent): void {
    const { schema, root } = this;
    if (!schema || !root || this.key !== event.key || event.kind !== DataNamespaceKind) {
      return;
    }
    if (event.operation !== 'patch') {
      this.processFilePatch((event as FilePatchBroadcastEvent).data);
    }
  }

  /**
   * Logic run when a patch operation is reported from the backend.
   * @param rev The revision info from the backend event
   */
  protected processFilePatch(rev: IPatchRevision): void {
    const { schema, root } = this;
    if (!schema || !root) {
      return;
    }
    const iData = JSON.parse(schema);
    const { id } = rev;
    const ownPatch = this.pendingPatches.get(id);
    if (ownPatch) {
      this.livePatches.set(id, rev);
      this.pendingPatches.delete(id);
      return;
    }
    const result = Patch.apply(iData, rev.patch);
    this.schema = JSON.stringify(result.doc);
    root.new(result.doc as IDataNamespace);
    this.render();
    const { nav } = this;
    if (nav) {
      nav.requestUpdate();
    }
  }

  /**
   * To be called when a change in the editor is detected.
   * It prepares the patch and stored it in the data store.
   */
  async updateSchema(): Promise<void> {
    if (this.updatingSchema) {
      this.isDirty = true;
      return;
    }
    this.isDirty = false;
    const { schema, root } = this;
    if (!root || !schema) {
      // eslint-disable-next-line no-console
      console.warn(`Trying to update the schema but none is set.`);
      return;
    }

    const newSchema = root.toJSON();
    const diff = Patch.diff(JSON.parse(schema), newSchema);
    if (!diff.length) {
      // eslint-disable-next-line no-console
      console.warn(`Expected to detect changes to the data schema.`);
      return;
    }
    this.updatingSchema = true;
    // gives 2.092279e+13 permutations.
    const id = randomString(16);
    this.pendingPatches.set(id, diff);
    try {
      await this.storePatch(root.key, id, diff, AppInfo);
      this.schema = JSON.stringify(newSchema);
    } catch (e) {
      const cause = e as Error;
      CoreEvents.Telemetry.exception(cause.message, true);
      this.reportCriticalError(cause.message);
    } finally {
      this.updatingSchema = false;
      if (this.isDirty) {
        this.updateSchema();
      }
    }
  }

  protected navigationSelectionHandler(e: CustomEvent): void {
    const { key, kind } = e.detail as ISelectDetail;
    const base = this.kindToRoute(kind);
    if (base) {
      navigate(base, key);
    }
  }

  protected kindToRoute(kind: string): string | undefined {
    switch (kind) {
      case DataEntityKind: return 'entity';
      case DataModelKind: return 'model';
      default: return undefined;
      // default: return 'namespace';
    }
  }

  protected entityChangeHandler(): void {
    this.updateSchema();
  }

  protected entityNameChangeHandler(): void {
    this.nav?.requestUpdate();
  }

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

  protected override headerTemplate(): TemplateResult {
    const { root } = this;
    const rootTitle = root && root.info && root.info.name;
    const title = rootTitle || 'Schema Designer';
    return html`
    <header>
      <h1 class="title-large">${title}</h1>
    </header>
    `;
  }

  protected override navigationTemplate(): TemplateResult {
    return html`
    <schema-design-navigation
      class="nav"
      .root="${this.root}" 
      .selected="${this.selected}" 
      @select="${this.navigationSelectionHandler}"
      ></schema-design-navigation>
    `;
  }

  protected override mainTemplate(): TemplateResult {
    const { page } = this;
    if (page === '404') {
      return this.e404Template();
    }
    return html`
    <main class="body-medium ${page}">
      <div class="content">
        ${this.renderPage()}
      </div>
    </main>`;
  }

  protected e404Template(): TemplateResult {
    return html`
    <main>
      <div class="full-error">
        <h2 class="headline-large">Not found</h2>
        <p class="body-large">
          The schema you are trying to open does not exist, was removed,
          or you lost access to it.
        </p>
        <ui-button type="filled" @click="${this.openStart}">Back to start</ui-button>
      </div>
    </main>`;
  }

  protected renderPage(): TemplateResult {
    switch (this.page) {
      // case 'namespace': return this._namespaceTemplate();
      case 'model': return this._dataModelTemplate();
      case 'entity': return this._entityTemplate();
      default: return this._emptySelectionTemplate();
    }
  }

  protected _emptySelectionTemplate(): TemplateResult {
    return html`
    <div class="empty-state body-large">
      <p>Schema designer allows you to design data and translate them into schemas which then can be used in other tools 
      to generate HTTP payloads and examples.</p>
      <p><b>Namespaces</b> helps you organize your data models. You can create as many namespaces as you need.</p>
      <p>Inside a namespaces put <b>data models</b>. They represent a logical structure of your data.</p>
      <p>An <b>entity</b> is a smallest part of your data. You can compose complex data structures through <b>entity associations</b>.</p>
    </div>
    `;
  }

  // protected _namespaceTemplate(): TemplateResult {
  //   return html`
  //   <p>TODO: Namespace view for ${this.selected}</p>
  //   `;
  // }

  protected _dataModelTemplate(): TemplateResult {
    return html`
    <data-model-visualization .root="${this.root}" .key="${this.selected}"></data-model-visualization>
    `;
  }

  protected _entityTemplate(): TemplateResult {
    return html`
    <data-entity-editor .root="${this.root}" .selected="${this.selected}" @change="${this.entityChangeHandler}" @namechange="${this.entityNameChangeHandler}"></data-entity-editor>
    `;
  }
}
