import {
  computed,
  inject,
  Injectable,
  InjectionToken,
  signal,
  untracked,
} from '@angular/core';
import { Config } from '@isa/core/config';
import { z } from 'zod';
import { configure } from 'scandit-web-datacapture-core';
import {
  barcodeCaptureLoader,
  Symbology,
} from 'scandit-web-datacapture-barcode';
import { Platform } from '@angular/cdk/platform';
import { PlatformNotSupportedError } from './errors/platform-not-supported.error';
import { logger } from '@isa/core/logging';
import { Overlay } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { ScannerComponent } from './scanner.component';
import { Subscription } from 'rxjs';

/**
 * Injection token for Scandit license key.
 * By default, retrieves the license key from the application config.
 */
export const SCANDIT_LICENSE = new InjectionToken<string>('ScanditLicense', {
  factory() {
    return inject(Config).get('licence.scandit', z.string());
  },
});

/**
 * Injection token for the Scandit library location.
 * By default, uses a 'scandit' path relative to the document base URI.
 */
export const SCANDIT_LIBRARY_LOCATION = new InjectionToken<string>(
  'ScanditLibraryLocation',
  {
    factory() {
      return new URL('scandit', document.baseURI).toString();
    },
  },
);

/**
 * Injection token for the default barcode symbologies to scan.
 * Provides a set of commonly used barcode formats.
 */
export const SCANDIT_DEFAULT_SYMBOLOGIES = new InjectionToken<Symbology[]>(
  'ScanditDefaultSymbologies',
  {
    factory() {
      return [
        Symbology.EAN8,
        Symbology.EAN13UPCA,
        Symbology.UPCE,
        Symbology.Code128,
        Symbology.Code39,
        Symbology.Code93,
        Symbology.InterleavedTwoOfFive,
        Symbology.QR,
      ];
    },
  },
);

/**
 * Enumeration of possible scanner status values.
 *
 * - None: Initial state, scanner not yet initialized
 * - Initializing: Scanner is in the process of initializing
 * - Ready: Scanner is fully initialized and ready to use
 * - Error: Scanner encountered an error during initialization
 */
export const ScannerStatus = {
  None: 'none',
  Initializing: 'initializing',
  Ready: 'ready',
  Error: 'error',
} as const;

export type ScannerStatus = (typeof ScannerStatus)[keyof typeof ScannerStatus];

/**
 * Configuration options for scanner operations.
 *
 * @property symbologies - Optional array of barcode symbologies to use for scanning
 * @property abortSignal - Optional AbortSignal to cancel an ongoing scan operation
 */
export type ScannerInputs = {
  symbologies?: Symbology[];
  abortSignal?: AbortSignal;
};

@Injectable({ providedIn: 'root' })
/**
 * Service that provides barcode scanning functionality using the Scandit SDK.
 * The service handles initialization, configuration, and scanning operations.
 */
export class ScannerService {
  #logger = logger(() => ({ service: 'ScannerService' }));
  #licenseKey = inject(SCANDIT_LICENSE);
  #platform = inject(Platform);
  #libraryLocation = inject(SCANDIT_LIBRARY_LOCATION);
  #defaultSymbologies = inject(SCANDIT_DEFAULT_SYMBOLOGIES);

  #overlay = inject(Overlay);
  #overlayRef = this.#overlay.create({
    hasBackdrop: true,
    backdropClass: 'scanner-backdrop',
    panelClass: 'scanner-panel',
    positionStrategy: this.#overlay
      .position()
      .global()
      .centerHorizontally()
      .centerVertically(),
    scrollStrategy: this.#overlay.scrollStrategies.block(),
  });

  /** Current status of the scanner */
  #status = signal<ScannerStatus>(ScannerStatus.None);

  /**
   * Signal that indicates whether the scanner is ready to use.
   * This will trigger configuration if the scanner has not been initialized.
   */
  ready = computed(() => {
    untracked(() => {
      this.configure();
    });
    return this.#status() === ScannerStatus.Ready;
  });

  /**
   * Checks if the current platform is supported for scanning.
   * Currently supports iOS and Android.
   *
   * @throws {PlatformNotSupportedError} If the current platform is not supported
   */
  private checkPlatformSupported() {
    if (this.#platform.IOS || this.#platform.ANDROID) {
      return;
    }

    throw new PlatformNotSupportedError();
  }

  /**
   * Configures the Scandit barcode scanner SDK.
   * This method must be called before attempting to use the scanner.
   *
   * @returns A promise that resolves when configuration is complete
   * @throws Will propagate any errors from the Scandit SDK
   */
  async configure() {
    const status = this.#status();
    if (
      status === ScannerStatus.Initializing ||
      status === ScannerStatus.Ready
    ) {
      return;
    }

    this.#status.set(ScannerStatus.Initializing);
    this.#logger.info('Scanner initializing');

    try {
      this.checkPlatformSupported();

      await configure({
        licenseKey: this.#licenseKey,
        libraryLocation: this.#libraryLocation,
        moduleLoaders: [barcodeCaptureLoader()],
      });

      this.#status.set(ScannerStatus.Ready);
      this.#logger.info('Scanner ready');
    } catch (error) {
      if (error instanceof PlatformNotSupportedError) {
        this.#logger.warn(error.message, () => ({
          error,
        }));
        return;
      }

      this.#status.set(ScannerStatus.Error);
      this.#logger.error('Failed to configure Scandit', error as Error);
      throw error;
    }
  }

  /**
   * Opens the scanner interface and initiates a scan operation.
   *
   * @param options - Optional configuration for the scan operation
   * @returns A promise that resolves to the scanned barcode value, or null if scanning was cancelled
   * @throws Will propagate errors from configure() if scanner initialization fails
   */
  async open(options?: ScannerInputs): Promise<string | null> {
    await this.configure();

    return new Promise((resolve, reject) => {
      try {
        const componentPortal = new ComponentPortal(ScannerComponent);
        const componentRef = this.#overlayRef.attach(componentPortal);

        componentRef.setInput(
          'symbologies',
          options?.symbologies ?? this.#defaultSymbologies,
        );

        const sub = new Subscription();

        function cleanup() {
          componentRef.destroy();
          options?.abortSignal?.removeEventListener('abort', handleAbort);
          sub.unsubscribe();
        }

        function handleScan(value: string | null) {
          cleanup();
          resolve(value);
        }

        function handleAbort() {
          cleanup();
          reject(new Error('Scanner aborted'));
        }

        sub.add(componentRef.instance.scan.subscribe(handleScan));
        options?.abortSignal?.addEventListener('abort', handleAbort);
      } catch (error) {
        this.#logger.error('Failed to scan', error as Error);
        reject(error);
      }
    });
  }
}
