/**
 * @license
 * Copyright 2016 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import {Subscription} from 'rxjs';
import {map, distinctUntilChanged} from 'rxjs/operators';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import '../../shared/gr-dropdown/gr-dropdown';
import '../../shared/gr-icon/gr-icon';
import '../gr-account-dropdown/gr-account-dropdown';
import '../gr-smart-search/gr-smart-search';
import {getBaseUrl, getDocsBaseUrl} from '../../../utils/url-util';
import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {getAdminLinks, NavLink} from '../../../utils/admin-nav-util';
import {
  AccountDetailInfo,
  RequireProperties,
  ServerInfo,
  TopMenuEntryInfo,
  TopMenuItemInfo,
} from '../../../types/common';
import {AuthType} from '../../../constants/constants';
import {DropdownLink} from '../../shared/gr-dropdown/gr-dropdown';
import {getAppContext} from '../../../services/app-context';
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, PropertyValues, html, css} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
import {fireEvent} from '../../../utils/event-util';
import {resolve} from '../../../models/dependency';
import {configModelToken} from '../../../models/config/config-model';

type MainHeaderLink = RequireProperties<DropdownLink, 'url' | 'name'>;

interface MainHeaderLinkGroup {
  title: string;
  links: MainHeaderLink[];
  class?: string;
}

const DEFAULT_LINKS: MainHeaderLinkGroup[] = [
  {
    title: 'Changes',
    links: [
      {
        url: '/q/status:open+-is:wip',
        name: 'Open',
      },
      {
        url: '/q/status:merged',
        name: 'Merged',
      },
      {
        url: '/q/status:abandoned',
        name: 'Abandoned',
      },
    ],
  },
];

const DOCUMENTATION_LINKS: MainHeaderLink[] = [
  {
    url: '/index.html',
    name: 'Table of Contents',
  },
  {
    url: '/user-search.html',
    name: 'Searching',
  },
  {
    url: '/user-upload.html',
    name: 'Uploading',
  },
  {
    url: '/access-control.html',
    name: 'Access Control',
  },
  {
    url: '/rest-api.html',
    name: 'REST API',
  },
  {
    url: '/intro-project-owner.html',
    name: 'Project Owner Guide',
  },
];

// Set of authentication methods that can provide custom registration page.
const AUTH_TYPES_WITH_REGISTER_URL: Set<AuthType> = new Set([
  AuthType.LDAP,
  AuthType.LDAP_BIND,
  AuthType.CUSTOM_EXTENSION,
]);

declare global {
  interface HTMLElementTagNameMap {
    'gr-main-header': GrMainHeader;
  }
}

@customElement('gr-main-header')
export class GrMainHeader extends LitElement {
  @property({type: String})
  searchQuery = '';

  @property({type: Boolean, reflect: true})
  loggedIn?: boolean;

  @property({type: Boolean, reflect: true})
  loading?: boolean;

  @property({type: String})
  loginUrl = '/login';

  @property({type: Boolean})
  mobileSearchHidden = false;

  // private but used in test
  @state() account?: AccountDetailInfo;

  @state() private adminLinks: NavLink[] = [];

  @state() private docBaseUrl: string | null = null;

  @state() private userLinks: MainHeaderLink[] = [];

  @state() private topMenus?: TopMenuEntryInfo[] = [];

  // private but used in test
  @state() registerText = 'Sign up';

  // Empty string means that the register <div> will be hidden.
  // private but used in test
  @state() registerURL = '';

  // private but used in test
  @state() feedbackURL = '';

  @state() private serverConfig?: ServerInfo;

  private readonly restApiService = getAppContext().restApiService;

  private readonly jsAPI = getAppContext().jsApiService;

  private readonly userModel = getAppContext().userModel;

  private readonly configModel = resolve(this, configModelToken);

  private subscriptions: Subscription[] = [];

  override connectedCallback() {
    super.connectedCallback();
    this.loadAccount();

    this.subscriptions.push(
      this.userModel.preferences$
        .pipe(
          map(preferences => preferences?.my ?? []),
          distinctUntilChanged()
        )
        .subscribe(items => {
          this.userLinks = items.map(this.createHeaderLink);
        })
    );
    this.subscriptions.push(
      this.configModel().serverConfig$.subscribe(config => {
        if (!config) return;
        this.serverConfig = config;
        this.retrieveFeedbackURL(config);
        this.retrieveRegisterURL(config);
        getDocsBaseUrl(config, this.restApiService).then(docBaseUrl => {
          this.docBaseUrl = docBaseUrl;
        });
      })
    );
  }

  override disconnectedCallback() {
    for (const s of this.subscriptions) {
      s.unsubscribe();
    }
    this.subscriptions = [];
    super.disconnectedCallback();
  }

  static override get styles() {
    return [
      sharedStyles,
      css`
        :host {
          display: block;
        }
        nav {
          align-items: center;
          display: flex;
        }
        .bigTitle {
          color: var(--header-text-color);
          font-size: var(--header-title-font-size);
          text-decoration: none;
        }
        .bigTitle:hover {
          text-decoration: underline;
        }
        .titleText::before {
          background-image: var(--header-icon);
          background-size: var(--header-icon-size) var(--header-icon-size);
          background-repeat: no-repeat;
          content: '';
          display: inline-block;
          height: var(--header-icon-size);
          margin-right: calc(var(--header-icon-size) / 4);
          vertical-align: text-bottom;
          width: var(--header-icon-size);
        }
        .titleText::after {
          content: var(--header-title-content);
        }
        ul {
          list-style: none;
          padding-left: var(--spacing-l);
        }
        .links > li {
          cursor: default;
          display: inline-block;
          padding: 0;
          position: relative;
        }
        .linksTitle {
          display: inline-block;
          font-weight: var(--font-weight-bold);
          position: relative;
          text-transform: uppercase;
        }
        .linksTitle:hover {
          opacity: 0.75;
        }
        .rightItems {
          align-items: center;
          display: flex;
          flex: 1;
          justify-content: flex-end;
        }
        .rightItems gr-endpoint-decorator:not(:empty) {
          margin-left: var(--spacing-l);
        }
        gr-smart-search {
          flex-grow: 1;
          margin: 0 var(--spacing-m);
          max-width: 500px;
          min-width: 150px;
        }
        gr-dropdown,
        .browse {
          padding: var(--spacing-m);
        }
        gr-dropdown {
          --gr-dropdown-item-color: var(--primary-text-color);
        }
        .settingsButton {
          margin-left: var(--spacing-m);
        }
        .feedbackButton {
          margin-left: var(--spacing-s);
        }
        .browse {
          color: var(--header-text-color);
          /* Same as gr-button */
          margin: 5px 4px;
          text-decoration: none;
        }
        .invisible,
        .settingsButton,
        gr-account-dropdown {
          display: none;
        }
        :host([loading]) .accountContainer,
        :host([loggedIn]) .loginButton,
        :host([loggedIn]) .registerButton {
          display: none;
        }
        :host([loggedIn]) .settingsButton,
        :host([loggedIn]) gr-account-dropdown {
          display: inline;
        }
        .accountContainer {
          align-items: center;
          display: flex;
          margin: 0 calc(0 - var(--spacing-m)) 0 var(--spacing-m);
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
        }
        .loginButton,
        .registerButton {
          padding: var(--spacing-m) var(--spacing-l);
        }
        .dropdown-trigger {
          text-decoration: none;
        }
        .dropdown-content {
          background-color: var(--view-background-color);
          box-shadow: var(--elevation-level-2);
        }
        /*
           * We are not using :host to do this, because :host has a lowest css priority
           * compared to others. This means that using :host to do this would break styles.
           */
        .linksTitle,
        .bigTitle,
        .loginButton,
        .registerButton,
        gr-icon,
        gr-dropdown,
        gr-account-dropdown {
          --gr-button-text-color: var(--header-text-color);
          color: var(--header-text-color);
        }
        #mobileSearch {
          display: none;
        }
        @media screen and (max-width: 50em) {
          .bigTitle {
            font-family: var(--header-font-family);
            font-size: var(--font-size-h3);
            font-weight: var(--font-weight-h3);
            line-height: var(--line-height-h3);
          }
          gr-smart-search,
          .browse,
          .rightItems .hideOnMobile,
          .links > li.hideOnMobile {
            display: none;
          }
          #mobileSearch {
            display: inline-flex;
          }
          .accountContainer {
            margin-left: var(--spacing-m) !important;
          }
          gr-dropdown {
            padding: var(--spacing-m) 0 var(--spacing-m) var(--spacing-m);
          }
        }
      `,
    ];
  }

  override render() {
    return html`
  <nav>
    <a href=${`//${window.location.host}${getBaseUrl()}/`} class="bigTitle">
      <gr-endpoint-decorator name="header-title">
        <span class="titleText"></span>
      </gr-endpoint-decorator>
    </a>
    <ul class="links">
      ${this.computeLinks(
        this.userLinks,
        this.adminLinks,
        this.topMenus,
        this.docBaseUrl
      ).map(linkGroup => this.renderLinkGroup(linkGroup))}
    </ul>
    <div class="rightItems">
      <gr-endpoint-decorator
        class="hideOnMobile"
        name="header-small-banner"
      ></gr-endpoint-decorator>
      <gr-smart-search
        id="search"
        label="Search for changes"
        .searchQuery=${this.searchQuery}
        .serverConfig=${this.serverConfig}
      ></gr-smart-search>
      <gr-endpoint-decorator
        class="hideOnMobile"
        name="header-browse-source"
      ></gr-endpoint-decorator>
      <gr-endpoint-decorator class="feedbackButton" name="header-feedback">
        ${this.renderFeedback()}
      </gr-endpoint-decorator>
      </div>
      ${this.renderAccount()}
    </div>
  </nav>
    `;
  }

  private renderLinkGroup(linkGroup: MainHeaderLinkGroup) {
    return html`
      <li class=${linkGroup.class ?? ''}>
        <gr-dropdown
          link
          down-arrow
          .items=${linkGroup.links}
          horizontal-align="left"
        >
          <span class="linksTitle" id=${linkGroup.title}>
            ${linkGroup.title}
          </span>
        </gr-dropdown>
      </li>
    `;
  }

  private renderFeedback() {
    if (!this.feedbackURL) return;

    return html`
      <a
        href=${this.feedbackURL}
        title="File a bug"
        aria-label="File a bug"
        target="_blank"
        role="button"
      >
        <gr-icon icon="bug_report" filled></gr-icon>
      </a>
    `;
  }

  private renderAccount() {
    return html`
      <div class="accountContainer" id="accountContainer">
        <div>
          <gr-icon
            id="mobileSearch"
            icon="search"
            @click=${(e: Event) => {
              this.onMobileSearchTap(e);
            }}
            role="button"
            aria-label=${this.mobileSearchHidden
              ? 'Show Searchbar'
              : 'Hide Searchbar'}
          ></gr-icon>
        </div>
        ${this.renderRegister()}
        <a class="loginButton" href=${this.loginUrl}>Sign in</a>
        <a
          class="settingsButton"
          href="${getBaseUrl()}/settings/"
          title="Settings"
          aria-label="Settings"
          role="button"
        >
          <gr-icon icon="settings" filled></gr-icon>
        </a>
        ${this.renderAccountDropdown()}
      </div>
    `;
  }

  private renderRegister() {
    if (!this.registerURL) return;

    return html`
      <div class="registerDiv">
        <a class="registerButton" href=${this.registerURL}>
          ${this.registerText}
        </a>
      </div>
    `;
  }

  private renderAccountDropdown() {
    if (!this.account) return;

    return html`
      <gr-account-dropdown .account=${this.account}></gr-account-dropdown>
    `;
  }

  override firstUpdated(changedProperties: PropertyValues) {
    super.firstUpdated(changedProperties);
    if (!this.getAttribute('role')) this.setAttribute('role', 'banner');
  }

  reload() {
    this.loadAccount();
  }

  // private but used in test
  computeLinks(
    userLinks?: MainHeaderLink[],
    adminLinks?: NavLink[],
    topMenus?: TopMenuEntryInfo[],
    docBaseUrl?: string | null,
    // defaultLinks parameter is used in tests only
    defaultLinks = DEFAULT_LINKS
  ) {
    if (
      userLinks === undefined ||
      adminLinks === undefined ||
      topMenus === undefined ||
      docBaseUrl === undefined
    ) {
      return [];
    }

    const links: MainHeaderLinkGroup[] = defaultLinks.map(menu => {
      return {
        title: menu.title,
        links: menu.links.slice(),
      };
    });
    if (userLinks && userLinks.length > 0) {
      links.push({
        title: 'Your',
        links: userLinks.slice(),
      });
    }
    const docLinks = this.getDocLinks(docBaseUrl, DOCUMENTATION_LINKS);
    if (docLinks.length) {
      links.push({
        title: 'Documentation',
        links: docLinks,
        class: 'hideOnMobile',
      });
    }
    links.push({
      title: 'Browse',
      links: adminLinks.slice(),
    });
    const topMenuLinks: {[name: string]: MainHeaderLink[]} = {};
    links.forEach(link => {
      topMenuLinks[link.title] = link.links;
    });
    for (const m of topMenus) {
      const items = m.items.map(this.createHeaderLink).filter(
        link =>
          // Ignore GWT project links
          !link.url.includes('${projectName}')
      );
      if (m.name in topMenuLinks) {
        items.forEach(link => {
          topMenuLinks[m.name].push(link);
        });
      } else if (items.length > 0) {
        links.push({
          title: m.name,
          links: (topMenuLinks[m.name] = items),
        });
      }
    }
    return links;
  }

  // private but used in test
  getDocLinks(docBaseUrl: string | null, docLinks: MainHeaderLink[]) {
    if (!docBaseUrl) {
      return [];
    }
    return docLinks.map(link => {
      let url = docBaseUrl;
      if (url && url[url.length - 1] === '/') {
        url = url.substring(0, url.length - 1);
      }
      return {
        url: url + link.url,
        name: link.name,
        target: '_blank',
      };
    });
  }

  // private but used in test
  loadAccount() {
    this.loading = true;

    return Promise.all([
      this.restApiService.getAccount(),
      this.restApiService.getTopMenus(),
      getPluginLoader().awaitPluginsLoaded(),
    ]).then(result => {
      const account = result[0];
      this.account = account;
      this.loggedIn = !!account;
      this.loading = false;
      this.topMenus = result[1];

      return getAdminLinks(
        account,
        () =>
          this.restApiService.getAccountCapabilities().then(capabilities => {
            if (!capabilities) {
              throw new Error('getAccountCapabilities returns undefined');
            }
            return capabilities;
          }),
        () => this.jsAPI.getAdminMenuLinks()
      ).then(res => {
        this.adminLinks = res.links;
      });
    });
  }

  // private but used in test
  retrieveFeedbackURL(config: ServerInfo) {
    if (config.gerrit?.report_bug_url) {
      this.feedbackURL = config.gerrit.report_bug_url;
    }
  }

  // private but used in test
  retrieveRegisterURL(config: ServerInfo) {
    if (AUTH_TYPES_WITH_REGISTER_URL.has(config.auth.auth_type)) {
      this.registerURL = config.auth.register_url ?? '';
      if (config.auth.register_text) {
        this.registerText = config.auth.register_text;
      }
    }
  }

  // private but used in test
  createHeaderLink(linkObj: TopMenuItemInfo): MainHeaderLink {
    // Delete target property due to complications of
    // https://bugs.chromium.org/p/gerrit/issues/detail?id=5888
    //
    // The server tries to guess whether URL is a view within the UI.
    // If not, it sets target='_blank' on the menu item. The server
    // makes assumptions that work for the GWT UI, but not PolyGerrit,
    // so we'll just disable it altogether for now.
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    const {target, ...headerLink} = {...linkObj};

    // Normalize all urls to PolyGerrit style.
    if (headerLink.url.startsWith('#')) {
      headerLink.url = linkObj.url.slice(1);
    }

    return headerLink;
  }

  private onMobileSearchTap(e: Event) {
    e.preventDefault();
    e.stopPropagation();
    fireEvent(this, 'mobile-search');
  }
}
