import app from '../../forum/app';
import Page, { IPageAttrs } from '../../common/components/Page';
import ItemList from '../../common/utils/ItemList';
import UserCard from './UserCard';
import SelectDropdown from '../../common/components/SelectDropdown';
import LinkButton from '../../common/components/LinkButton';
import Separator from '../../common/components/Separator';
import listItems from '../../common/helpers/listItems';
import AffixedSidebar from './AffixedSidebar';
import type User from '../../common/models/User';
import type Mithril from 'mithril';
import PageStructure from './PageStructure';

export interface IUserPageAttrs extends IPageAttrs {}

/**
 * Users whose profile has been fetched from the user endpoint, as opposed to
 * arriving in some other payload. See `UserPage.loadUser`.
 */
const profileLoaded = new WeakSet<User>();

/**
 * The `UserPage` component shows a user's profile. It can be extended to show
 * content inside of the content area. See `ActivityPage` and `SettingsPage` for
 * examples.
 *
 * @abstract
 */
export default class UserPage<CustomAttrs extends IUserPageAttrs = IUserPageAttrs, CustomState = undefined> extends Page<CustomAttrs, CustomState> {
  /**
   * The user this page is for.
   */
  user: User | null = null;

  oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
    super.oninit(vnode);

    this.bodyClass = 'App--user';
  }

  /**
   * Base view template for the user page.
   */
  view() {
    return (
      <PageStructure className="UserPage" hero={this.hero.bind(this)} sidebar={this.sidebar.bind(this)} loading={!this.user}>
        {this.user && this.content()}
      </PageStructure>
    );
  }

  hero() {
    return (
      <UserCard
        user={this.user}
        className="Hero UserHero"
        editable={this.user!.canEdit() || this.user!.canEditAvatar()}
        controlsButtonClassName="Button"
      />
    );
  }

  sidebar() {
    return (
      <AffixedSidebar>
        <nav className="sideNav UserPage-nav">
          <ul>{listItems(this.sidebarItems().toArray())}</ul>
        </nav>
      </AffixedSidebar>
    );
  }

  /**
   * Get the content to display in the user page.
   */
  content(): Mithril.Children | void {}

  /**
   * Initialize the component with a user, and trigger the loading of their
   * activity feed.
   *
   * @protected
   */
  show(user: User): void {
    this.user = user;

    app.current.set('user', user);

    app.setTitle(user.displayName());

    app.history.push(app.current.get('routeName'), user.displayName());

    m.redraw();
  }

  /**
   * Given a username, load the user's profile from the store, or make a request
   * if we don't have it yet. Then initialize the profile page with that user.
   *
   * A user in the store is shown straight away, but the record may have arrived in
   * another endpoint's payload, carrying only the relationships that endpoint
   * included. Every user has `joinTime` whichever endpoint it came from, so that
   * says nothing about what else is loaded: whatever the user endpoint includes by
   * default, which is anything an extension adds to a profile, can be missing. So
   * such a user is fetched in the background, once, and the response updates the
   * same record the page is showing.
   *
   * Resolves once `this.user` is set so that subclasses can safely chain
   * dependent work (e.g. fetching related resources keyed off the user id). It
   * does not wait for that background fetch.
   */
  loadUser(username: string): Promise<void> {
    const lowercaseUsername = username.toLowerCase();

    // On initial render the server preloads the target user as the primary
    // resource of the API document, so prefer that. It stays correct under
    // any slug driver (including IdWithDisplayName, where the route slug
    // does not equal the username).
    const preloaded = app.preloadedApiDocument<User>();

    if (preloaded && !Array.isArray(preloaded) && preloaded.joinTime()) {
      // It is the profile itself, with the profile's includes.
      profileLoaded.add(preloaded);

      this.show(preloaded);
      return Promise.resolve();
    }

    app.store.all<User>('users').some((user) => {
      if ((user.username().toLowerCase() === lowercaseUsername || user.id() === username || user.slug() === username) && user.joinTime()) {
        this.show(user);
        return true;
      }

      return false;
    });

    if (this.user) {
      this.completeUser(this.user);

      return Promise.resolve();
    }

    return app.store.find<User>('users', username, { bySlug: true }).then((user) => {
      profileLoaded.add(user);

      this.show(user);
    });
  }

  /**
   * Fetch a user from the user endpoint, unless that has already been done, to
   * fill in what the payload they first arrived in did not carry.
   *
   * It is silent: the page already has a user to show, so a failure is not worth an
   * alert, and the next visit tries again.
   */
  protected completeUser(user: User): void {
    if (profileLoaded.has(user)) return;

    profileLoaded.add(user);

    app.store.find<User>('users', user.slug(), { bySlug: true }, { errorHandler: () => {} }).catch(() => profileLoaded.delete(user));
  }

  /**
   * Build an item list for the content of the sidebar.
   */
  sidebarItems() {
    const items = new ItemList<Mithril.Children>();

    items.add(
      'nav',
      <SelectDropdown className="App-titleControl" buttonClassName="Button">
        {this.navItems().toArray()}
      </SelectDropdown>
    );

    return items;
  }

  /**
   * Build an item list for the navigation in the sidebar.
   */
  navItems() {
    const items = new ItemList<Mithril.Children>();
    const user = this.user!;
    const isActor = app.session.user === user;

    items.add(
      'posts',
      <LinkButton href={app.route('user.posts', { username: user.slug() })} icon="far fa-comment">
        {app.translator.trans('core.forum.user.posts_link')} <span className="Button-badge">{user.commentCount()}</span>
      </LinkButton>,
      100
    );

    items.add(
      'discussions',
      <LinkButton href={app.route('user.discussions', { username: user.slug() })} icon="fas fa-bars">
        {app.translator.trans('core.forum.user.discussions_link')} <span className="Button-badge">{user.discussionCount()}</span>
      </LinkButton>,
      90
    );

    if (isActor) {
      items.add('separator', <Separator />, -90);
      items.add(
        'settings',
        <LinkButton href={app.route('settings')} icon="fas fa-cog">
          {app.translator.trans('core.forum.user.settings_link')}
        </LinkButton>,
        -100
      );
    }

    if (isActor || app.forum.attribute<boolean>('canModerateAccessTokens')) {
      if (!isActor) {
        items.add('security-separator', <Separator />, -90);
      }

      items.add(
        'security',
        <LinkButton href={app.route('user.security', { username: user.slug() })} icon="fas fa-shield-alt">
          {app.translator.trans('core.forum.user.security_link')}
        </LinkButton>,
        -100
      );
    }

    return items;
  }
}
