import * as plugins from '../../plugins.js';
import { logger } from '../../logging.js';
import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js';
import { requireAdminIdentity } from '../helpers/guards.js';
import { getErrorMessage } from '../../utils/error.js';
import { toApiData, toApiDataArray } from '../helpers/serialization.js';

type IAppStoreUpgradeOperation = interfaces.requests.IAppStoreUpgradeOperation;
type TAppStoreUpgradeStep = interfaces.requests.TAppStoreUpgradeStep;

export class AppStoreHandler {
  public typedrouter = new plugins.typedrequest.TypedRouter();
  private upgradeOperations = new Map<string, IAppStoreUpgradeOperation>();

  constructor(private opsServerRef: OpsServer) {
    this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
    this.registerHandlers();
  }

  private getUpgradeOperations(): IAppStoreUpgradeOperation[] {
    return Array.from(this.upgradeOperations.values())
      .sort((a, b) => b.startedAt - a.startedAt)
      .slice(0, 25);
  }

  private pruneUpgradeOperations(): void {
    const operations = Array.from(this.upgradeOperations.values())
      .sort((a, b) => b.startedAt - a.startedAt);
    for (const operation of operations.slice(100)) {
      this.upgradeOperations.delete(operation.id);
    }
  }

  public getUpgradeOperationsForService(serviceNameArg: string): IAppStoreUpgradeOperation[] {
    return this.getUpgradeOperations().filter((operationArg) => operationArg.serviceName === serviceNameArg);
  }

  public clearOperationsForService(serviceNameArg: string): void {
    for (const operation of this.upgradeOperations.values()) {
      if (operation.serviceName === serviceNameArg) {
        this.upgradeOperations.delete(operation.id);
      }
    }
  }

  private getSafeAppStoreErrorMessage(errorArg: unknown): string {
    const message = getErrorMessage(errorArg);
    const lowerMessage = message.toLowerCase();
    if (
      lowerMessage.includes('fetch') ||
      lowerMessage.includes('connect') ||
      lowerMessage.includes('connection refused') ||
      lowerMessage.includes('network') ||
      /http \d+/.test(lowerMessage)
    ) {
      return 'The App Store backend is currently unreachable. Please retry later.';
    }
    if (
      lowerMessage.includes('domain is required') ||
      lowerMessage.includes('missing required app env var') ||
      lowerMessage.includes('invalid app id') ||
      lowerMessage.includes('invalid service name') ||
      lowerMessage.includes('invalid install service port') ||
      lowerMessage.includes('app requires onebox')
    ) {
      return message;
    }
    return 'The App Store request failed. Please retry later.';
  }

  private createSafeAppStoreTypedError(actionArg: string, errorArg: unknown): plugins.typedrequest.TypedResponseError {
    logger.warn(`${actionArg}: ${getErrorMessage(errorArg)}`);
    return new plugins.typedrequest.TypedResponseError(
      `${actionArg}: ${this.getSafeAppStoreErrorMessage(errorArg)}`,
    );
  }

  public async startHostedAppUpgrade(serviceNameArg: string, targetVersionArg: string): Promise<IAppStoreUpgradeOperation> {
    const operation = await this.createUpgradeOperation(serviceNameArg, targetVersionArg);
    void this.performUpgrade(operation.id).catch(() => {});
    return operation;
  }

  private getRunningUpgrade(serviceNameArg: string): IAppStoreUpgradeOperation | null {
    for (const operation of this.upgradeOperations.values()) {
      if (operation.serviceName === serviceNameArg && operation.status === 'running') {
        return operation;
      }
    }
    return null;
  }

  private async createUpgradeOperation(
    serviceNameArg: string,
    targetVersionArg: string,
  ): Promise<IAppStoreUpgradeOperation> {
    const existingRunning = this.getRunningUpgrade(serviceNameArg);
    if (existingRunning) {
      throw new plugins.typedrequest.TypedResponseError(
        `An upgrade is already running for ${serviceNameArg}`,
      );
    }

    const existingService = await this.opsServerRef.oneboxRef.database.getServiceByName(serviceNameArg);
    if (!existingService) {
      throw new plugins.typedrequest.TypedResponseError(`Service not found: ${serviceNameArg}`);
    }
    if (!existingService.appTemplateId) {
      throw new plugins.typedrequest.TypedResponseError('Service was not deployed from an app template');
    }
    if (!existingService.appTemplateVersion) {
      throw new plugins.typedrequest.TypedResponseError('Service has no tracked template version');
    }

    const now = Date.now();
    const operation: IAppStoreUpgradeOperation = {
      id: crypto.randomUUID(),
      serviceName: existingService.name,
      appTemplateId: existingService.appTemplateId,
      fromVersion: existingService.appTemplateVersion,
      targetVersion: targetVersionArg,
      status: 'running',
      step: 'queued',
      progressLines: [`Queued upgrade ${existingService.appTemplateVersion} -> ${targetVersionArg}`],
      warnings: [],
      startedAt: now,
      updatedAt: now,
    };

    this.upgradeOperations.set(operation.id, operation);
    this.pruneUpgradeOperations();
    await this.pushUpgradeProgress(operation);
    return operation;
  }

  private async updateUpgradeOperation(
    operationIdArg: string,
    stepArg: TAppStoreUpgradeStep,
    messageArg: string,
    updatesArg: Partial<IAppStoreUpgradeOperation> = {},
  ): Promise<IAppStoreUpgradeOperation> {
    const existing = this.upgradeOperations.get(operationIdArg);
    if (!existing) {
      throw new Error(`Upgrade operation not found: ${operationIdArg}`);
    }

    const nextOperation: IAppStoreUpgradeOperation = {
      ...existing,
      ...updatesArg,
      step: stepArg,
      updatedAt: Date.now(),
      progressLines: [...existing.progressLines, messageArg].slice(-200),
    };
    this.upgradeOperations.set(operationIdArg, nextOperation);
    this.pruneUpgradeOperations();
    await this.pushUpgradeProgress(nextOperation);
    return nextOperation;
  }

  private async pushUpgradeProgress(operationArg: IAppStoreUpgradeOperation): Promise<void> {
    await this.opsServerRef.pushDashboardEvent('pushAppStoreUpgradeProgress', {
      operation: operationArg,
    });
  }

  private async performUpgrade(operationIdArg: string): Promise<interfaces.data.IService> {
    let operation = this.upgradeOperations.get(operationIdArg);
    if (!operation) {
      throw new Error(`Upgrade operation not found: ${operationIdArg}`);
    }

    try {
      operation = await this.updateUpgradeOperation(
        operation.id,
        'validating',
        `Validating ${operation.serviceName} for App Store upgrade`,
      );

      const existingService = await this.opsServerRef.oneboxRef.database.getServiceByName(operation.serviceName);
      if (!existingService) {
        throw new Error(`Service not found: ${operation.serviceName}`);
      }
      if (!existingService.appTemplateId || !existingService.appTemplateVersion) {
        throw new Error('Service is missing App Store template metadata');
      }

      logger.info(
        `Upgrading service '${operation.serviceName}' from v${operation.fromVersion} to v${operation.targetVersion}`,
      );

      await this.updateUpgradeOperation(
        operation.id,
        'migration',
        `Resolving migration for ${operation.appTemplateId} ${operation.fromVersion} -> ${operation.targetVersion}`,
      );

      const migrationResult = await this.opsServerRef.oneboxRef.appStore.executeMigration(
        existingService,
        operation.fromVersion,
        operation.targetVersion,
      );

      if (!migrationResult.success) {
        throw new Error(`Migration failed: ${migrationResult.warnings.join('; ')}`);
      }

      if (migrationResult.warnings.length > 0) {
        operation = await this.updateUpgradeOperation(
          operation.id,
          'migration',
          `Migration completed with ${migrationResult.warnings.length} warning(s)`,
          { warnings: migrationResult.warnings },
        );
      }

      await this.updateUpgradeOperation(
        operation.id,
        'applying',
        `Applying upgrade to ${operation.serviceName}`,
      );

      const updatedService = toApiData(await this.opsServerRef.oneboxRef.appStore.applyUpgrade(
        operation.serviceName,
        migrationResult,
        operation.targetVersion,
        {
          onProgress: async (progressArg) => {
            await this.updateUpgradeOperation(
              operation!.id,
              progressArg.step as TAppStoreUpgradeStep,
              progressArg.message,
            );
          },
        },
      ));

      await this.updateUpgradeOperation(
        operation.id,
        'complete',
        `Upgrade completed for ${operation.serviceName}`,
        {
          status: 'success',
          completedAt: Date.now(),
          service: updatedService,
          warnings: migrationResult.warnings,
        },
      );

      return updatedService;
    } catch (error) {
      await this.updateUpgradeOperation(
        operation.id,
        'failed',
        `Upgrade failed: ${getErrorMessage(error)}`,
        {
          status: 'failed',
          completedAt: Date.now(),
          error: getErrorMessage(error),
        },
      );
      throw error;
    }
  }

  private registerHandlers(): void {
    this.typedrouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAppStoreTemplates>(
        'getAppStoreTemplates',
        async (dataArg) => {
          await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
          const apps = await this.opsServerRef.oneboxRef.appStore.getApps();
          return {
            apps,
            sourceInfo: this.opsServerRef.oneboxRef.appStore.getSourceInfo(),
          };
        },
      ),
    );

    this.typedrouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAppStoreConfig>(
        'getAppStoreConfig',
        async (dataArg) => {
          await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
          try {
            const config = await this.opsServerRef.oneboxRef.appStore.getAppVersionConfig(
              dataArg.appId,
              dataArg.version,
            );
            const appMeta = await this.opsServerRef.oneboxRef.appStore.getAppMeta(dataArg.appId);
            return { config, appMeta };
          } catch (error) {
            throw this.createSafeAppStoreTypedError(
              `Could not load App Store details for ${dataArg.appId}@${dataArg.version || 'latest'}`,
              error,
            );
          }
        },
      ),
    );

    this.typedrouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_InstallAppStoreApp>(
        'installAppStoreApp',
        async (dataArg) => {
          await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
          try {
            const service = await this.opsServerRef.oneboxRef.appStore.installApp(dataArg.install);
            return { service: toApiData(service) };
          } catch (error) {
            throw this.createSafeAppStoreTypedError(
              `Could not install App Store app ${dataArg.install?.appId || 'unknown'}`,
              error,
            );
          }
        },
      ),
    );

    this.typedrouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetUpgradeableAppStoreServices>(
        'getUpgradeableAppStoreServices',
        async (dataArg) => {
          await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
          const services = await this.opsServerRef.oneboxRef.appStore.getUpgradeableAppStoreServices();
          return { services: toApiDataArray(services) };
        },
      ),
    );

    this.typedrouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpgradeAppStoreService>(
        'upgradeAppStoreService',
        async (dataArg) => {
          await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
          const operation = await this.createUpgradeOperation(dataArg.serviceName, dataArg.targetVersion);
          const updatedService = await this.performUpgrade(operation.id);
          const completedOperation = this.upgradeOperations.get(operation.id)!;

          return {
            service: toApiData(updatedService),
            warnings: completedOperation.warnings,
          };
        },
      ),
    );

    this.typedrouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_StartAppStoreServiceUpgrade>(
        'startAppStoreServiceUpgrade',
        async (dataArg) => {
          await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
          const operation = await this.createUpgradeOperation(dataArg.serviceName, dataArg.targetVersion);
          void this.performUpgrade(operation.id).catch(() => {});
          return { operation };
        },
      ),
    );

    this.typedrouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAppStoreUpgradeOperations>(
        'getAppStoreUpgradeOperations',
        async (dataArg) => {
          await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
          return { operations: this.getUpgradeOperations() };
        },
      ),
    );
  }
}
