/*
 * Copyright (C) 2023 StApps
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation, version 3.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <https://www.gnu.org/licenses/>.
 */
import {Component, DestroyRef, inject, Input, OnInit} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {Keyboard} from '@capacitor/keyboard';
import {
  AlertController,
  AnimationBuilder,
  AnimationController,
  ModalController,
} from '@ionic/angular/standalone';
import {Capacitor} from '@capacitor/core';
import {
  SCFacet,
  SCFeatureConfiguration,
  SCSearchFilter,
  SCSearchQuery,
  SCSearchSort,
  SCThings,
} from '@openstapps/core';
import {NGXLogger} from 'ngx-logger';
import {combineLatest, Subject} from 'rxjs';
import {debounceTime, distinctUntilChanged, startWith} from 'rxjs/operators';
import {ContextMenuService} from '../../menu/context/context-menu.service';
import {SettingsProvider} from '../../settings/settings.provider';
import {DataRoutingService} from '../data-routing.service';
import {DataProvider} from '../data.provider';
import {PositionService} from '../../map/position.service';
import {ConfigProvider} from '../../config/config.provider';
import {searchPageSwitchAnimation} from './search-page-switch-animation';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {ContextMenuModalComponent} from '../../menu/context/context-menu-modal.component';
import {enterAnimation, leaveAnimation} from '../../menu/context/context-menu-modal.animations';

/**
 * SearchPageComponent queries things and shows list of things as search results and filter as context menu
 */
@Component({
  selector: 'stapps-search-page',
  templateUrl: 'search-page.html',
  styleUrls: ['search-page.scss'],
  providers: [ContextMenuService],
})
export class SearchPageComponent implements OnInit {
  @Input() title = 'search.title';

  @Input() placeholder = 'search.search_bar.placeholder';

  @Input() searchInstruction = 'search.instruction';

  @Input() backUrl?: string;

  /**
   * Signalizes that the data is being loaded
   */
  loading = false;

  /**
   * Navigation elements between search pages
   */
  @Input() navigation: Array<{label: string; routerLink?: string[]}> = [
    {label: 'search.type'},
    {label: 'hebisSearch.type', routerLink: ['/hebis-search']},
  ];

  /**
   * Show default data (e.g. when there is user interaction)
   */
  @Input() showDefaultData = false;

  /**
   * Show the navigation drawer
   */
  @Input() showDrawer = true;

  /**
   * Show "universal search" toolbar
   */
  @Input() showTopToolbar = true;

  @Input() showContextMenu = true;

  /**
   * Api query filter
   */
  filterQuery: SCSearchFilter | undefined;

  /**
   * Filters the search should be initialized with
   */
  @Input() forcedFilter?: SCSearchFilter;

  /**
   * If routing should be done if the user clicks on an item
   */
  @Input() itemRouting? = true;

  /**
   * Thing counter to start query the next page from
   */
  from = 0;

  /**
   * Container for queried things
   */
  items: Promise<SCThings[]>;

  /**
   * Page size of queries
   */
  @Input() pageSize = 30;

  /**
   * Search value from search bar
   */
  queryText: string;

  /**
   * Emits when there is a change in the query (search, sort or filter changed)
   */
  queryChanged = new Subject<void>();

  /**
   * Subject to handle search text changes
   */
  queryTextChanged = new Subject<string>();

  /**
   * Time to wait for search query if search text is changing
   */
  searchQueryDueTime = 1000;

  /**
   * Search response only ever contains a single SCThingType
   */
  singleTypeResponse = false;

  /**
   * Api query sorting
   */
  sortQuery: SCSearchSort[] | undefined;

  destroy$ = inject(DestroyRef);

  routeAnimation: AnimationBuilder;

  /**
   * Injects the providers and creates subscriptions
   * @param alertController AlertController
   * @param dataProvider DataProvider
   * @param contextMenuService ContextMenuService
   * @param settingsProvider SettingsProvider
   * @param logger An angular logger
   * @param dataRoutingService DataRoutingService
   * @param router Router
   * @param route ActivatedRoute
   * @param positionService PositionService
   * @param configProvider ConfigProvider
   */
  constructor(
    protected readonly alertController: AlertController,
    protected dataProvider: DataProvider,
    protected readonly contextMenuService: ContextMenuService,
    protected readonly settingsProvider: SettingsProvider,
    protected readonly logger: NGXLogger,
    protected dataRoutingService: DataRoutingService,
    protected router: Router,
    private readonly route: ActivatedRoute,
    protected positionService: PositionService,
    private readonly configProvider: ConfigProvider,
    protected animationController: AnimationController,
    protected modalController: ModalController,
  ) {
    this.routeAnimation = searchPageSwitchAnimation(animationController);
  }

  /**
   * Fetches items with set query configuration
   * @param append If true fetched data gets appended to existing, override otherwise (default false)
   */
  protected async fetchAndUpdateItems(append = false): Promise<void> {
    // build query search options
    const searchOptions: SCSearchQuery = {
      from: this.from,
      size: this.pageSize,
    };
    const filters: SCSearchFilter[] = [];

    if (this.queryText && this.queryText.length > 0) {
      // add query string
      searchOptions.query = this.queryText;
    }

    if (this.sortQuery) {
      // add query sorting
      searchOptions.sort = this.sortQuery;
    }

    for (const filter of [this.forcedFilter, this.filterQuery]) {
      if (filter !== undefined) {
        filters.push(filter);
      }
    }
    if (filters.length > 0) {
      searchOptions.filter = {
        arguments: {
          filters: filters,
          operation: 'and',
        },
        type: 'boolean',
      };
    }

    this.loading = !append;

    try {
      const result = await this.dataProvider.search(searchOptions);
      this.singleTypeResponse = result.facets.find(facet => facet.field === 'type')?.buckets.length === 1;
      if (append) {
        // append results
        this.items = this.items.then(it =>
          // fix for some very short results
          it.length === result.pagination.total ? it : [...it, ...result.data],
        );
      } else {
        // override items with results
        this.updateContextFilter(result.facets);
        this.items = Promise.resolve(result.data);
      }
    } catch (error) {
      this.logger.error(error);
    } finally {
      this.loading = false;
    }
  }

  /**
   * Hides keyboard in native app environments
   */
  hideKeyboard() {
    if (Capacitor.isNativePlatform()) {
      Keyboard.hide();
    }
  }

  /**
   * Set starting values (e.g. forced filter, which can be set in components inheriting this one)
   */
  // eslint-disable-next-line class-methods-use-this
  initialize() {
    // nothing to do here
  }

  /**
   * Loads next page of things
   */
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  async loadMore(): Promise<void> {
    this.from += this.pageSize;
    await this.fetchAndUpdateItems(true);
  }

  /**
   * Search event of search bar
   */
  searchStringChanged(queryValue: string) {
    this.router.navigate([], {
      relativeTo: this.route,
      queryParams: {query: queryValue},
      queryParamsHandling: 'merge',
    });
    this.queryTextChanged.next(queryValue);
  }

  /**
   * Updates the possible filter options in ContextMenuService with facets
   */
  updateContextFilter(facets: SCFacet[]) {
    this.contextMenuService.updateContextFilter(facets);
  }

  ngOnInit(defaultListeners = true) {
    this.initialize();
    this.contextMenuService.setContextSort({
      name: 'sort',
      reversed: false,
      value: 'relevance',
      values: [
        {
          reversible: false,
          value: 'relevance',
        },
        {
          reversible: true,
          value: 'name',
        },
        {
          reversible: true,
          value: 'type',
        },
      ],
    });

    if (defaultListeners) {
      combineLatest([
        this.queryTextChanged.pipe(
          debounceTime(this.searchQueryDueTime),
          distinctUntilChanged(),
          startWith(this.queryText),
        ),
        this.contextMenuService.filterQueryChanged$.pipe(startWith(this.filterQuery)),
        this.contextMenuService.sortQueryChanged$.pipe(startWith(this.sortQuery)),
      ])
        .pipe(takeUntilDestroyed(this.destroy$))
        .subscribe(async query => {
          this.queryText = query[0];
          this.filterQuery = query[1];
          this.sortQuery = query[2];
          this.from = 0;
          if (this.filterQuery !== undefined || this.queryText?.length > 0 || this.showDefaultData) {
            await this.fetchAndUpdateItems();
            this.queryChanged.next();
          }
        });
      this.settingsProvider.settingsActionChanged$
        .pipe(takeUntilDestroyed(this.destroy$))
        .subscribe(({type, payload}) => {
          if (type === 'stapps.settings.changed') {
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            const {category, name, value} = payload!;
            this.logger.log(`received event "settings.changed" with category:
            ${category}, name: ${name}, value: ${JSON.stringify(value)}`);
          }
        });
      this.dataRoutingService
        .itemSelectListener()
        .pipe(takeUntilDestroyed(this.destroy$))
        .subscribe(item => {
          if (this.itemRouting) {
            void this.router.navigate(['/data-detail', item.uid], {state: {item}});
          }
        });
    }
    try {
      // TODO: make this hack more generic
      if (this.navigation[1]?.routerLink?.[0] === '/hebis-search') {
        const features = this.configProvider.getValue('features') as SCFeatureConfiguration;
        if (features.plugins?.['hebis-plugin']?.urlPath === undefined) {
          this.navigation = [];
        }
      }
    } catch (error) {
      this.logger.error(error);
    }
  }

  /**
   * Initialize
   */
  async ionViewWillEnter() {
    const term = this.route.snapshot.queryParamMap.get('query') || undefined;
    if (term) {
      this.queryText = term;
      this.searchStringChanged(term);
    }
  }

  async openContextMenu(): Promise<void> {
    const modal = await this.modalController.create({
      component: ContextMenuModalComponent,
      cssClass: 'context-menu-modal',
      showBackdrop: true,
      backdropDismiss: true,
      mode: 'md', // TODO has to be fixed to md for now. ios mode is unresponsive. Remove if Issue 239 is solved
      enterAnimation: (baseElement: HTMLElement) => enterAnimation(baseElement, this.animationController),
      leaveAnimation: (baseElement: HTMLElement) => leaveAnimation(baseElement, this.animationController),
      componentProps: {
        contextMenuService: this.contextMenuService,
      },
    });

    await modal.present();
  }
}
