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

export class SettingsHandler {
  public typedrouter = new plugins.typedrequest.TypedRouter();

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

  private async getSettingsObject(): Promise<interfaces.data.ISettings> {
    const db = this.opsServerRef.oneboxRef.database;
    const cloudflareToken = await db.getSecretSetting('cloudflareToken');
    const dcrouterGatewayApiToken = await db.getSecretSetting('dcrouterGatewayApiToken');
    const settingsMap = await db.getAllSettings();
    const managedDcRouter = this.opsServerRef.oneboxRef.managedDcRouter;

    return {
      cloudflareToken: cloudflareToken || '',
      cloudflareZoneId: settingsMap['cloudflareZoneId'] || '',
      adminUiDomain: settingsMap['adminUiDomain'] || '',
      dcrouterMode: await managedDcRouter.getMode(),
      dcrouterManagedImage: await managedDcRouter.getImage(),
      dcrouterManagedOpsPort: await managedDcRouter.getOpsPort(),
      dcrouterManagedHttpPort: await managedDcRouter.getHttpPort(),
      dcrouterManagedHttpsPort: await managedDcRouter.getHttpsPort(),
      dcrouterManagedDataDir: await managedDcRouter.getDataDir(),
      dcrouterGatewayUrl: settingsMap['dcrouterGatewayUrl'] || '',
      dcrouterGatewayApiToken: dcrouterGatewayApiToken || '',
      dcrouterGatewayClientId: settingsMap['dcrouterGatewayClientId'] || '',
      dcrouterTargetHost: settingsMap['dcrouterTargetHost'] || '',
      dcrouterTargetPort: parseInt(settingsMap['dcrouterTargetPort'] || '0', 10),
      platformStorageClass: settingsMap['platformStorageClass'] || await db.getDefaultStorageClassName('database'),
      autoRenewCerts: settingsMap['autoRenewCerts'] === 'true',
      renewalThreshold: parseInt(settingsMap['renewalThreshold'] || '30', 10),
      acmeEmail: settingsMap['acmeEmail'] || '',
      httpPort: parseInt(settingsMap['httpPort'] || '80', 10),
      httpsPort: parseInt(settingsMap['httpsPort'] || '443', 10),
      forceHttps: settingsMap['forceHttps'] === 'true',
    };
  }

  private registerHandlers(): void {
    this.typedrouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSettings>(
        'getSettings',
        async (dataArg) => {
          await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
          const settings = await this.getSettingsObject();
          return { settings };
        },
      ),
    );

    this.typedrouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateSettings>(
        'updateSettings',
        async (dataArg) => {
          await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
          const db = this.opsServerRef.oneboxRef.database;
          const updates = dataArg.settings;

          const normalizedUpdates = this.normalizeUpdates(updates);
          if (Object.prototype.hasOwnProperty.call(normalizedUpdates, 'adminUiDomain')) {
            try {
              await this.opsServerRef.oneboxRef.assertAdminUiDomainCompatible(
                String(normalizedUpdates.adminUiDomain || ''),
              );
            } catch (error) {
              throw new plugins.typedrequest.TypedResponseError(
                getErrorMessage(error),
              );
            }
          }


          // Store each setting as key-value pair
          for (const [key, value] of Object.entries(normalizedUpdates)) {
            if (value !== undefined) {
              if (db.isSecretSettingKey(key)) {
                await db.setSecretSetting(key, String(value));
              } else {
                await db.setSetting(key, String(value));
              }
            }
          }

          if (this.hasRouteSyncSetting(normalizedUpdates)) {
            this.refreshGatewayRoutes(normalizedUpdates).catch((error) => {
              logger.warn(`dcrouter gateway settings refresh failed: ${getErrorMessage(error)}`);
            });
          }

          const settings = await this.getSettingsObject();
          return { settings };
        },
      ),
    );

    this.typedrouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SetBackupPassword>(
        'setBackupPassword',
        async (dataArg) => {
          await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
          await this.opsServerRef.oneboxRef.database.setSecretSetting('backupPassword', dataArg.password);
          return { ok: true };
        },
      ),
    );

    this.typedrouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetBackupPasswordStatus>(
        'getBackupPasswordStatus',
        async (dataArg) => {
          await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
          const isConfigured = await this.opsServerRef.oneboxRef.database.hasSecretSetting('backupPassword');
          return { status: { isConfigured } };
        },
      ),
    );

    this.typedrouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetStorageConfig>(
        'getStorageConfig',
        async (dataArg) => {
          await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
          const storageTargets = await this.opsServerRef.oneboxRef.database.getAllStorageTargets();
          const storageClasses = await this.opsServerRef.oneboxRef.database.getAllStorageClasses();
          return {
            storageTargets: toApiDataArray(storageTargets),
            storageClasses: toApiDataArray(storageClasses),
          };
        },
      ),
    );

    this.typedrouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpsertStorageTarget>(
        'upsertStorageTarget',
        async (dataArg) => {
          await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
          const storageTarget = await this.opsServerRef.oneboxRef.database.upsertStorageTarget(dataArg.storageTarget);
          return { storageTarget: toApiData(storageTarget) };
        },
      ),
    );

    this.typedrouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteStorageTarget>(
        'deleteStorageTarget',
        async (dataArg) => {
          await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
          await this.opsServerRef.oneboxRef.database.deleteStorageTarget(dataArg.name);
          return { ok: true };
        },
      ),
    );

    this.typedrouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpsertStorageClass>(
        'upsertStorageClass',
        async (dataArg) => {
          await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
          const storageClass = await this.opsServerRef.oneboxRef.database.upsertStorageClass(dataArg.storageClass);
          return { storageClass: toApiData(storageClass) };
        },
      ),
    );

    this.typedrouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteStorageClass>(
        'deleteStorageClass',
        async (dataArg) => {
          await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
          await this.opsServerRef.oneboxRef.database.deleteStorageClass(dataArg.name);
          return { ok: true };
        },
      ),
    );
  }

  private normalizeUpdates(
    settings: Partial<interfaces.data.ISettings>,
  ): Partial<interfaces.data.ISettings> {
    const normalizedUpdates = { ...settings };
    if (Object.prototype.hasOwnProperty.call(normalizedUpdates, 'adminUiDomain')) {
      const normalizedDomain = normalizePasskeyHostname(String(normalizedUpdates.adminUiDomain || ''));
      if (!normalizedDomain) {
        throw new plugins.typedrequest.TypedResponseError('Invalid Admin UI domain');
      }
      normalizedUpdates.adminUiDomain = normalizedDomain;
    }
    return normalizedUpdates;
  }

  private hasRouteSyncSetting(settings: Partial<interfaces.data.ISettings>): boolean {
    return [
      'adminUiDomain',
      'dcrouterMode',
      'dcrouterManagedImage',
      'dcrouterManagedOpsPort',
      'dcrouterManagedHttpPort',
      'dcrouterManagedHttpsPort',
      'dcrouterManagedDataDir',
      'dcrouterGatewayUrl',
      'dcrouterGatewayApiToken',
      'dcrouterGatewayClientId',
      'dcrouterTargetHost',
      'dcrouterTargetPort',
    ].some((key) => Object.prototype.hasOwnProperty.call(settings, key));
  }

  private hasManagedDcRouterRuntimeSetting(settings: Partial<interfaces.data.ISettings>): boolean {
    return [
      'dcrouterMode',
      'dcrouterManagedImage',
      'dcrouterManagedOpsPort',
      'dcrouterManagedHttpPort',
      'dcrouterManagedHttpsPort',
      'dcrouterManagedDataDir',
    ].some((key) => Object.prototype.hasOwnProperty.call(settings, key));
  }

  private async refreshGatewayRoutes(settings: Partial<interfaces.data.ISettings>): Promise<void> {
    const onebox = this.opsServerRef.oneboxRef;
    if (this.hasManagedDcRouterRuntimeSetting(settings)) {
      if (await onebox.managedDcRouter.getMode() === 'managed') {
        await onebox.managedDcRouter.restart();
      } else {
        await onebox.managedDcRouter.stop();
      }
    }

    await onebox.reverseProxy.reloadRoutes();
    await onebox.externalGateway.syncDomains();
    await onebox.externalGateway.syncServiceRoutes();
  }
}
