/* eslint-disable import/no-extraneous-dependencies */
/* eslint-disable class-methods-use-this */
import { html, TemplateResult } from 'lit';
import { IUser, Events as CoreEvents, IBackendInfo } from '@api-client/core/build/browser.js';
import { RenderableMixin } from '../mixins/RenderableMixin.js';
import { RouteMixin } from '../mixins/RouteMixin.js';
import { reactive } from '../lib/decorators.js';
import { Events } from '../events/Events.js';
import { EventTypes } from '../events/EventTypes.js';
import { ConfigBroadcast, ConfigBroadcastChannel, IConfigEnvironment, ThemeType } from '../lib/config/Config.js';
import supportedPlatform from '../lib/SupportedPlatform.js';
import { ButtonType } from '../ui/button/UiButton.js';
import { StoreBindings } from '../bindings/base/StoreBindings.js';
import type { AddConfigEnvironmentInit, ConfigurationBindings } from '../bindings/base/ConfigurationBindings.js';
import { PlatformBindings } from '../bindings/base/PlatformBindings.js';
import { SnackNotifications } from '../ui/notification/SnackNotifications.js';
import '../define/ui/ui-button.js';

export interface ApplicationInit {
  /**
   * A reference to the store bindings.
   */
  store: StoreBindings;
  /**
   * A reference to the config bindings.
   */
  config: ConfigurationBindings;
}

interface ApplicationBindings {
  /**
   * A reference to the store bindings.
   */
  store: StoreBindings;
  /**
   * A reference to the config bindings.
   */
  config: ConfigurationBindings;
  [key: string]: PlatformBindings;
}

/**
 * A base class for pages build outside the LitElement. It uses `lit-html` 
 * as the template renderer.
 * 
 * The implementation (extending this class) should override the `pageTemplate()`
 * function that returns the `TemplateResult` from the `lit-html` library.
 * 
 * To reflect the changed state call the `render()` function. The function schedules
 * a micro task (through `requestAnimationFrame`) to call the render function on the template.
 * 
 * Use the `@reactive()` decorator from `src/lib/decorators.js` to mark a property as reactive,
 * meaning, when the property change it calls the `render()` function.
 */
export abstract class ApplicationScreen extends RouteMixin(RenderableMixin(EventTarget)) {
  @reactive() eventTarget: EventTarget = window;

  /** 
   * True when the app should render mobile friendly view.
   */
  @reactive() isMobile = false;

  /** 
   * The loading state information.
   */
  @reactive() loadingStatus = 'Initializing the application...';

  /**
   * A flag telling the application screen that the logic is initialized.
   * 
   * The page can request different initialization logics. When the logic is
   * loaded the flag is set to true.
   */
  @reactive() initialized = false;

  /**
   * The page on the screen currently being rendered.
   */
  @reactive() page?: string;

  /**
   * The current user.
   * Call the `loadUser()` to populate this.
   */
  @reactive() user?: IUser;

  /**
   * True when the user meta is being loaded.
   */
  @reactive() protected loadingUser = false;
  
  @reactive() protected authenticated = false;

  /**
   * This is automatically set when initializing the store.
   * Basic information about the store.
   */
  protected storeInfo?: IBackendInfo;

  /**
   * Checks whether the store is in the single user mode.
   * It also returns true when the store info is not set (before the backend was initialized).
   */
  get isSingleUser(): boolean {
    const { storeInfo } = this;
    if (!storeInfo) {
      return true;
    }
    return storeInfo.mode === 'single-user';
  }

  protected pendingResolver?: (value: void | PromiseLike<void>) => void;

  /**
   * Detected system theme.
   */
  systemTheme: 'light' | 'dark' = 'light';

  /**
   * The theme to use as selected by the user.
   * Default to system default.
   */
  userTheme: ThemeType = 'default';

  /**
   * Application configuration broadcast channel.
   */
  configBroadcast = new BroadcastChannel(ConfigBroadcastChannel);

  bindings: ApplicationBindings;

  /**
   * The store environment in use.
   * This is set after the `initializeStore()` was called.
   */
  env?: IConfigEnvironment;

  constructor(init: ApplicationInit) {
    super();
    this.bindings = {
      config: init.config,
      store: init.store,
    };
    window.onunhandledrejection = this.unhandledRejectionHandler.bind(this);
    this.initMediaQueries();
    this.initStoreChange();
    this.configBroadcast.addEventListener('message', this.handleConfigBroadcast.bind(this));
    // this will use the system-default theme until config is read.
    this.themeActiveCallback();
  }

  /**
   * Called once when the page is being initialized.
   */
  abstract initialize(): Promise<void>;

  /**
   * Initializes configuration that is relevant to all application windows.
   */
  async initializeConfig(): Promise<void> {
    const { config } = this.bindings;
    const theme = await config.local.get('theme') as ThemeType | undefined;
    if (theme) {
      this.userTheme = theme;
    }
    this.themeActiveCallback();
  }

  /**
   * Initializes media queries and observers.
   */
  initMediaQueries(): void {
    const mql = window.matchMedia('(max-width: 600px)');
    this.isMobile = mql.matches;
    mql.addEventListener('change', (e) => {
      this.isMobile = e.matches;
    });

    const dark = window.matchMedia('(prefers-color-scheme: dark)');
    if (dark.matches) {
      this.systemTheme = 'dark';
    }
    dark.addEventListener('change', (e) => {
      this.systemTheme = e.matches ? 'dark' : 'light';
      this.themeActiveCallback();
    });
  }

  protected themeActiveCallback(): void {
    const { systemTheme, userTheme } = this;
    let theme: 'light' | 'dark';
    if (userTheme === 'default') {
      theme = systemTheme;
    } else {
      theme = userTheme;
    }
    this.activateTheme(theme);
  }

  protected activateTheme(type: 'light' | 'dark'): void {
    const root = document.querySelector('html');
    if (!root) {
      return;
    }
    const { classList } = root;
    // clear all themes
    classList.forEach((value) => {
      if (value.startsWith('theme-')) {
        classList.remove(value);
      }
    });
    const name = `theme-${type}`;
    classList.add(name);
  }

  async isPlatformSupported(): Promise<boolean> {
    const result = await supportedPlatform();
    if (!result) {
      this.page = 'unsupported-platform';
    }
    return result;
  }

  initStoreChange(): void {
    window.addEventListener(EventTypes.Config.Environment.State.defaultChange, (e: Event) => {
      const event = e as CustomEvent;
      const id = event.detail.id as string;
      this.storeChanged(id);
    });
  }

  /**
   * Override in a child class to implement a logic that runs when the default 
   * store change in the configuration.
   * 
   * @param id The id of the activated environment. When missing it means there's no default environment.
   */
  abstract storeChanged(id?: string): Promise<void>;

  /**
   * Creates a modal dialog with the error details.
   * @param message The message to render
   */
  reportCriticalError(message: string): void {
    try {
      const dialog = document.createElement('dialog');
      const form = document.createElement('form');
      const title = document.createElement('h2');
      const content = document.createElement('div');
      const messageContainer = document.createElement('p');
      const buttons = document.createElement('div');
      const button = document.createElement('ui-button');
      dialog.classList.add('error-dialog');
      form.method = 'method';
      title.innerText = `An error ocurred`;
      messageContainer.classList.add('body-medium');
      messageContainer.textContent = message;
      content.appendChild(messageContainer);
      button.textContent = 'Dismiss';
      button.type = ButtonType.filled;
      button.value = 'submit';
      button.submit = true;
      buttons.classList.add('dialog-buttons');
      buttons.appendChild(button);
      form.appendChild(title);
      form.appendChild(content);
      form.appendChild(buttons);
      dialog.appendChild(form);
      document.body.appendChild(dialog);
      dialog.showModal();
      form.addEventListener('submit', (e: SubmitEvent) => {
        e.preventDefault();
        dialog.close();
      });
      dialog.addEventListener('close', () => {
        dialog.parentElement?.removeChild(dialog);
      });
    } catch (e) {
      /* eslint-disable-next-line no-console */  
      console.error(e);
    }
  }

  unhandledRejectionHandler(e: PromiseRejectionEvent): void {
    /* eslint-disable-next-line no-console */
    console.error(e);
    this.reportCriticalError(e.reason);
  }

  protected handleConfigBroadcast(e: MessageEvent): void {
    const event = e.data as ConfigBroadcast;
    const { path } = event;
    if (path === 'local.set') {
      this.handleLocalConfigUpdated(event.key as string, event.value as unknown);
    } else if (path === 'environment.setDefault') {
      this.handleDefaultEnvironmentSet(event.env as IConfigEnvironment);
    } else if (path === 'environment.add') {
      this.handleEnvironmentAdd(event.env as IConfigEnvironment, event.init as AddConfigEnvironmentInit);
    }
  }

  protected handleLocalConfigUpdated(key: string, value: unknown): void {
    if (key === 'theme') {
      this.userTheme = value as ThemeType;
      this.themeActiveCallback();
    }
  }

  protected handleDefaultEnvironmentSet(env: IConfigEnvironment): void {
    const { initialized, bindings } = this;
    this.env = env;
    bindings.store.global.set(env);
    this.resolvePending();
    if (initialized) {
      this.storeChanged(env.key);
    }
  }

  protected handleEnvironmentAdd(env: IConfigEnvironment, init: AddConfigEnvironmentInit = {}): void {
    if (!init.asDefault) {
      return;
    }
    this.handleDefaultEnvironmentSet(env);
  }

  /**
   * Called from the initialize store init flow when the user is not authenticated
   * and the auth screen is rendered.
   */
  protected async handleAuthenticate(): Promise<void> {
    const { store } = this.bindings;
    await store.auth.authenticate({ updateEnvironment: true });
    this.authenticated = true;
    this.resolvePending();
  }

  protected handleOpenStoreConfig(): void {
    Events.Navigation.Store.config();
  }

  /**
   * Initializes the flow where an environment is required.
   * This checks for the default environment.
   * When it doesn't exists it asks the user to pick existing environment 
   * or to create one.
   * When a default environment exists, it performs authentication
   * when necessary. 
   * 
   * Eventually it returns the HTTP store with an authenticated environment.
   * 
   * @param defaultFlows When set to `true` is renders default pages for env selector 
   * or when to authenticate. When false it throws errors instead. Default to `true`.
   */
  protected async initializeStore(defaultFlows = true): Promise<void> {
    const { store, config } = this.bindings;
    let env: IConfigEnvironment | undefined;
    try {
      env = await config.environment.read();
    } catch (e) { 
      if (!defaultFlows) {
        throw e;
      }
    }
    if (!env || !env.location) {
      await this.enterEnvironmentMissingFlow();
    }
    if (!env || !env.location) {
      return;
    }
    this.env = env;
    await store.global.set(env);
    try {
      this.storeInfo = await store.environment.readStoreInfo(env.location);
    } catch (e) {
      if (!defaultFlows) {
        throw e;
      }
      await this.enterStoreUnavailableFlow();
    }
    const authStatus = await store.auth.isAuthenticated();
    if (!authStatus) {
      if (!defaultFlows) {
        throw new Error(`Not authenticated.`);
      }
      await this.enterAuthenticationRequiredFlow();
    }
    this.authenticated = true;
  }

  async loadUser(): Promise<void> {
    this.loadingUser = true;
    try {
      this.user = await this.bindings.store.users.me();
    } catch (e) {
      CoreEvents.Telemetry.exception(`Loading user: ${(e as Error).message}`, undefined, this.eventTarget || window);
    } finally {
      this.loadingUser = false;
    }
  }

  private enterEnvironmentMissingFlow(): Promise<void> {
    return new Promise((resolve) => {
      this.pendingResolver = resolve;
      this.page = 'env-required';
    });
  }

  private enterAuthenticationRequiredFlow(): Promise<void> {
    return new Promise((resolve) => {
      this.pendingResolver = resolve;
      this.page = 'auth-required';
    });
  }
  

  /**
   * Enters into the flow where the application is unable to connect to the data store.
   * It asks user to retry or re-configure.
   * 
   * Note, this is called from withing the `initializeStore()` method and this is expected to
   * eventually finish.
   */
  private enterStoreUnavailableFlow(): Promise<void> {
    return new Promise((resolve) => {
      this.pendingResolver = resolve;
      this.page = 'store-unavailable';
    });
  }

  protected async handleRetryStoreInfo(): Promise<void> {
    const { bindings, env } = this;
    const { store } = bindings;
    if (!env) {
      // this should not happen as we enter this flow after the env is set.
      return;
    }
    try {
      this.storeInfo = await store.environment.readStoreInfo(env.location);
      this.resolvePending();
    } catch (_) {
      SnackNotifications.notify(`Unable to connect.`);
    }
  }

  private resolvePending(): void {
    const { pendingResolver } = this;
    if (pendingResolver) {
      this.pendingResolver = undefined;
      pendingResolver();
    }
  }

  /**
   * @returns A template for the page loader
   */
  loaderTemplate(): TemplateResult {
    return html`
    <div class="app-loader body-large">
      <p class="message">Preparing something spectacular</p>
      <p class="sub-message">${this.loadingStatus}</p>
    </div>
    `;
  }

  override pageTemplate(): TemplateResult {
    const { initialized } = this;
    if (!initialized) {
      if (this.page) {
        return html`
        ${this.renderInitFlowPage()}
        `;
      }
      return this.loaderTemplate();
    }
    return html``;
  }

  protected headerTemplate(): TemplateResult {
    return html`
    <header>Welcome</header>
    `;
  }

  protected mainTemplate(): TemplateResult {
    return html`<main></main>`;
  }

  protected footerTemplate(): TemplateResult {
    return html`<footer>Credits: Pawel Uchida-Psztyc</footer>`;
  }

  protected navigationTemplate(): TemplateResult {
    return html`<nav></nav>`;
  }

  protected renderInitFlowPage(): TemplateResult {
    switch (this.page) {
      case 'auth-required': return this.renderAuthRequired();
      case 'env-required': return this.renderEnvRequired();
      case 'unsupported-platform': return this.renderPlatformNotSupported();
      case 'store-unavailable': return this.renderStoreUnavailable();
      default: return html`<p class="general-error">Unknown state. Did you set the <i>initialized</i> flag?</p>`;
    }
  }

  /**
   * Renders a full page overlay with the authentication required message.
   */
  protected renderAuthRequired(): TemplateResult {
    return html`
    <div class="auth-required-screen">
      <h1 class="display-large">Authentication required</h1>
      <p class="body-large">You are not authenticated. To continue, please, authenticate your account.</p>
      <ui-button type="tonal" @click="${this.handleAuthenticate}">Authenticate</ui-button>
    </div>
    `;
  }

  protected renderEnvRequired(): TemplateResult {
    return html`
    <div class="auth-required-screen">
      <h1 class="display-large">Environment configuration missing</h1>
      <p class="body-large">
        The application is unable to determine which environment to use.
        Open the <b>store configuration</b> screen to configure the connection.
      </p>
      <ui-button type="tonal" @click="${this.handleOpenStoreConfig}">Configure store</ui-button>
    </div>
    `;
  }

  protected renderPlatformNotSupported(): TemplateResult {
    return html`
    <div class="auth-required-screen">
      <h1 class="display-large">Unsupported platform</h1>
      <p class="body-large">
        It looks like your browser does not support one of the crucial features needed to run
        this application. Accept our apologies, we work hard to support as many browsers as possible. 
      </p>
    </div>
    `;
  }

  protected renderStoreUnavailable(): TemplateResult {
    return html`
    <div class="auth-required-screen">
      <h1 class="display-large">Unable to connect</h1>
      <p class="body-large">
        It looks like you or the data store are offline. The application is unable to continue
        without a connection to the backend.
      </p>
      <div class="actions">
        <ui-button type="tonal" @click="${this.handleRetryStoreInfo}">Retry</ui-button>
        <ui-button type="outlined" @click="${this.handleOpenStoreConfig}">Configure store</ui-button>
      </div>
    </div>
    `;
  }
}
