import { inject, Injectable } from '@angular/core';
import { BehaviorSubject, Observable, of, firstValueFrom } from 'rxjs';
import { map, filter, withLatestFrom } from 'rxjs/operators';
import { BranchDTO } from '@generated/swagger/checkout-api';
import { isBoolean, isNumber } from '@utils/common';
import { ApplicationService } from './application.service';
import { TabService } from '@isa/core/tabs';
import { ApplicationProcess } from './defs/application-process';
import { Tab, TabMetadata } from '@isa/core/tabs';
import { toObservable } from '@angular/core/rxjs-interop';
import { Store } from '@ngrx/store';
import { removeProcess } from './store/application.actions';

/**
 * Adapter service that bridges the old ApplicationService interface with the new TabService.
 *
 * This adapter allows existing code that depends on ApplicationService to work with the new
 * TabService without requiring immediate code changes. It maps ApplicationProcess concepts
 * to Tab entities, storing process-specific data in tab metadata.
 *
 * Key mappings:
 * - ApplicationProcess.id <-> Tab.id
 * - ApplicationProcess.name <-> Tab.name
 * - ApplicationProcess metadata (section, type, etc.) <-> Tab.metadata with 'process_' prefix
 * - ApplicationProcess.data <-> Tab.metadata with 'data_' prefix
 *
 * @example
 * ```typescript
 * // Inject the adapter instead of the original service
 * constructor(private applicationService: ApplicationServiceAdapter) {}
 *
 * // Use the same API as before
 * const process = await this.applicationService.createCustomerProcess();
 * this.applicationService.activateProcess(process.id);
 * ```
 */
@Injectable({ providedIn: 'root' })
export class ApplicationServiceAdapter extends ApplicationService {
  #store = inject(Store);

  #tabService = inject(TabService);

  #activatedProcessId$ = toObservable(this.#tabService.activatedTabId);

  #tabs$ = toObservable(this.#tabService.entities);

  #processes$ = this.#tabs$.pipe(
    map((tabs) => tabs.map((tab) => this.mapTabToProcess(tab))),
  );

  #section = new BehaviorSubject<'customer' | 'branch'>('customer');

  readonly REGEX_PROCESS_NAME = /^Vorgang \d+$/;

  get activatedProcessId() {
    return this.#tabService.activatedTabId();
  }

  get activatedProcessId$() {
    return this.#activatedProcessId$;
  }

  getProcesses$(
    section?: 'customer' | 'branch',
  ): Observable<ApplicationProcess[]> {
    return this.#processes$.pipe(
      map((processes) =>
        processes.filter((process) =>
          section ? process.section === section : true,
        ),
      ),
    );
  }

  getProcessById$(processId: number): Observable<ApplicationProcess> {
    return this.#processes$.pipe(
      map((processes) => processes.find((process) => process.id === processId)),
    );
  }

  getSection$(): Observable<'customer' | 'branch'> {
    return this.#section.asObservable();
  }

  getTitle$(): Observable<'Kundenbereich' | 'Filialbereich'> {
    return this.getSection$().pipe(
      map((section) =>
        section === 'customer' ? 'Kundenbereich' : 'Filialbereich',
      ),
    );
  }

  /** @deprecated */
  getActivatedProcessId$(): Observable<number> {
    return this.activatedProcessId$;
  }

  activateProcess(activatedProcessId: number): void {
    this.#tabService.activateTab(activatedProcessId);
  }

  removeProcess(processId: number): void {
    this.#tabService.removeTab(processId);
    this.#store.dispatch(removeProcess({ processId }));
  }

  patchProcess(processId: number, changes: Partial<ApplicationProcess>): void {
    const tabChanges: {
      name?: string;
      tags?: string[];
      metadata?: Record<string, unknown>;
    } = {};

    if (changes.name) {
      tabChanges.name = changes.name;
    }

    // Store other ApplicationProcess properties in metadata
    const metadataKeys = [
      'section',
      'type',
      'closeable',
      'confirmClosing',
      'created',
      'activated',
      'data',
    ];
    metadataKeys.forEach((key) => {
      if (tabChanges.metadata === undefined) {
        tabChanges.metadata = {};
      }

      if (changes[key as keyof ApplicationProcess] !== undefined) {
        tabChanges.metadata[`process_${key}`] =
          changes[key as keyof ApplicationProcess];
      }
    });

    // Apply the changes to the tab
    this.#tabService.patchTab(processId, tabChanges);
  }

  patchProcessData(processId: number, data: Record<string, unknown>): void {
    const currentProcess = this.#tabService.entityMap()[processId];
    const currentData: TabMetadata =
      (currentProcess?.metadata?.['process_data'] as TabMetadata) ?? {};

    this.#tabService.patchTab(processId, {
      metadata: { [`process_data`]: { ...currentData, ...data } },
    });
  }

  getSelectedBranch$(): Observable<BranchDTO> {
    return this.#processes$.pipe(
      withLatestFrom(this.#activatedProcessId$),
      map(([processes, activatedProcessId]) =>
        processes.find((process) => process.id === activatedProcessId),
      ),
      filter((process): process is ApplicationProcess => !!process),
      map((process) => process.data?.selectedBranch as BranchDTO),
    );
  }

  async createCustomerProcess(processId?: number): Promise<ApplicationProcess> {
    const processes = await firstValueFrom(this.getProcesses$('customer'));

    const processIds = processes
      .filter((x) => this.REGEX_PROCESS_NAME.test(x.name))
      .map((x) => +x.name.split(' ')[1]);

    const maxId = processIds.length > 0 ? Math.max(...processIds) : 0;

    const process: ApplicationProcess = {
      id: processId ?? Date.now(),
      type: 'cart',
      name: `Vorgang ${maxId + 1}`,
      section: 'customer',
      closeable: true,
    };

    await this.createProcess(process);
    return process;
  }

  /**
   * Creates a new ApplicationProcess by first creating a Tab and then storing
   * process-specific properties in the tab's metadata.
   *
   * @param process - The ApplicationProcess to create
   * @throws {Error} If process ID already exists or is invalid
   */
  async createProcess(process: ApplicationProcess): Promise<void> {
    const existingProcess = this.#tabService.entityMap()[process.id];
    if (existingProcess?.id === process?.id) {
      throw new Error('Process Id existiert bereits');
    }

    if (!isNumber(process.id)) {
      throw new Error('Process Id nicht gesetzt');
    }

    if (!isBoolean(process.closeable)) {
      process.closeable = true;
    }

    if (!isBoolean(process.confirmClosing)) {
      process.confirmClosing = true;
    }

    process.created = this.createTimestamp();
    process.activated = 0;

    // Create tab with process data and preserve the process ID
    this.#tabService.addTab({
      id: process.id,
      name: process.name,
      tags: [process.section, process.type].filter(Boolean),
      metadata: {
        process_section: process.section,
        process_type: process.type,
        process_closeable: process.closeable,
        process_confirmClosing: process.confirmClosing,
        process_created: process.created,
        process_activated: process.activated,
        process_data: process.data,
      },
    });
  }

  setSection(section: 'customer' | 'branch'): void {
    this.#section.next(section);
  }

  getLastActivatedProcessWithSectionAndType$(
    section: 'customer' | 'branch',
    type: string,
  ): Observable<ApplicationProcess> {
    return this.getProcesses$(section).pipe(
      map((processes) =>
        processes
          ?.filter((process) => process.type === type)
          ?.reduce((latest, current) => {
            if (!latest) {
              return current;
            }
            return latest?.activated > current?.activated ? latest : current;
          }, undefined),
      ),
    );
  }

  getLastActivatedProcessWithSection$(
    section: 'customer' | 'branch',
  ): Observable<ApplicationProcess> {
    return this.getProcesses$(section).pipe(
      map((processes) =>
        processes?.reduce((latest, current) => {
          if (!latest) {
            return current;
          }
          return latest?.activated > current?.activated ? latest : current;
        }, undefined),
      ),
    );
  }

  /**
   * Maps Tab entities to ApplicationProcess objects by extracting process-specific
   * metadata and combining it with tab properties.
   *
   * @param tab - The tab entity to convert
   * @returns The corresponding ApplicationProcess object
   */
  private mapTabToProcess(tab: Tab): ApplicationProcess {
    return {
      id: tab.id,
      name: tab.name,
      created:
        this.getMetadataValue<number>(tab.metadata, 'process_created') ??
        tab.createdAt,
      activated:
        this.getMetadataValue<number>(tab.metadata, 'process_activated') ??
        tab.activatedAt ??
        0,
      section:
        this.getMetadataValue<'customer' | 'branch'>(
          tab.metadata,
          'process_section',
        ) ?? 'customer',
      type: this.getMetadataValue<string>(tab.metadata, 'process_type'),
      closeable:
        this.getMetadataValue<boolean>(tab.metadata, 'process_closeable') ??
        true,
      confirmClosing:
        this.getMetadataValue<boolean>(
          tab.metadata,
          'process_confirmClosing',
        ) ?? true,
      data: this.extractDataFromMetadata(tab.metadata),
    };
  }

  /**
   * Extracts ApplicationProcess data properties from tab metadata.
   * Data properties are stored with a 'data_' prefix in tab metadata.
   *
   * @param metadata - The tab metadata object
   * @returns The extracted data object or undefined if no data properties exist
   */
  private extractDataFromMetadata(
    metadata: TabMetadata,
  ): Record<string, unknown> | undefined {
    // Return the complete data object stored under 'process_data'
    const processData = metadata?.['process_data'];

    if (
      processData &&
      typeof processData === 'object' &&
      processData !== null
    ) {
      return processData as Record<string, unknown>;
    }

    return undefined;
  }

  private getMetadataValue<T>(
    metadata: TabMetadata,
    key: string,
  ): T | undefined {
    return metadata?.[key] as T | undefined;
  }

  private createTimestamp(): number {
    return Date.now();
  }
}
