import { validateCronExpression } from 'cron';
import { createZodDto } from 'nestjs-zod';
import { SystemConfig } from 'src/config';
import {
  CLIPConfigSchema,
  DuplicateDetectionConfigSchema,
  FacialRecognitionConfigSchema,
  OcrConfigSchema,
} from 'src/dtos/model-config.dto';
import {
  AudioCodecSchema,
  ColorspaceSchema,
  CQModeSchema,
  HlsVideoResolutionSchema,
  ImageFormatSchema,
  LogLevelSchema,
  OAuthTokenEndpointAuthMethodSchema,
  ToneMappingSchema,
  TranscodeHardwareAccelerationSchema,
  TranscodePolicySchema,
  VideoCodecSchema,
  VideoContainerSchema,
} from 'src/enum';
import z from 'zod';

/** Coerces 'true'/'false' strings to boolean, but also allows booleans. */
const configBool = z
  .preprocess((val) => {
    if (val === 'true') {
      return true;
    }
    if (val === 'false') {
      return false;
    }
    return val;
  }, z.boolean())
  .meta({ type: 'boolean' });

const JobSettingsSchema = z
  .object({
    concurrency: z.int().min(1).describe('Concurrency'),
  })
  .meta({ id: 'JobSettingsDto' });

const cronExpressionSchema = z
  .string()
  .superRefine((value, ctx) => {
    const validated = validateCronExpression(value);
    if (!validated.valid) {
      ctx.addIssue({
        code: 'custom',
        message: `Invalid cron expression. ${validated.error?.message ?? ''}`,
        input: value,
      });
    }
  })
  .describe('Cron expression');

const DatabaseBackupSchema = z
  .object({
    enabled: configBool.describe('Enabled'),
    cronExpression: cronExpressionSchema,
    keepLastAmount: z.int().min(1).describe('Keep last amount'),
  })
  .meta({ id: 'DatabaseBackupConfig' });

const SystemConfigIntegrityJobSchema = z
  .object({
    enabled: z.boolean().describe('Enabled'),
    cronExpression: cronExpressionSchema.describe('Cron expression for when the integrity check should run'),
  })
  .describe('Integrity job config')
  .meta({ id: 'SystemConfigIntegrityJob' });

const SystemConfigIntegrityChecksumJobSchema = SystemConfigIntegrityJobSchema.extend({
  timeLimit: z.int().nonnegative().describe('How long the integrity checksum job may run for'),
  percentageLimit: z
    .float32()
    .nonnegative()
    .max(1)
    .describe('Percentage limit of the integrity checksum job')
    .meta({ format: 'double' }),
})
  .describe('Integrity checksum job config')
  .meta({ id: 'SystemConfigIntegrityChecksumJob' });

const SystemConfigIntegrityChecksSchema = z
  .object({
    missingFiles: SystemConfigIntegrityJobSchema,
    untrackedFiles: SystemConfigIntegrityJobSchema,
    checksumFiles: SystemConfigIntegrityChecksumJobSchema,
  })
  .describe('Integrity checks config')
  .meta({ id: 'SystemConfigIntegrityChecks' });

const SystemConfigBackupsSchema = z.object({ database: DatabaseBackupSchema }).meta({ id: 'SystemConfigBackupsDto' });

const SystemConfigFFmpegSchema = z
  .object({
    crf: z.coerce.number().int().min(0).max(51).describe('CRF'),
    threads: z.coerce.number().int().min(0).describe('Threads'),
    preset: z.string().describe('Preset'),
    targetVideoCodec: VideoCodecSchema,
    acceptedVideoCodecs: z.array(VideoCodecSchema).describe('Accepted video codecs'),
    targetAudioCodec: AudioCodecSchema,
    acceptedAudioCodecs: z.array(AudioCodecSchema).describe('Accepted audio codecs'),
    acceptedContainers: z.array(VideoContainerSchema).describe('Accepted containers'),
    targetResolution: z.string().describe('Target resolution'),
    maxBitrate: z.string().describe('Max bitrate'),
    bframes: z.coerce.number().int().min(-1).max(16).describe('B-frames'),
    refs: z.coerce.number().int().min(0).max(6).describe('References'),
    gopSize: z.coerce.number().int().min(0).describe('GOP size'),
    temporalAQ: configBool.describe('Temporal AQ'),
    cqMode: CQModeSchema,
    twoPass: configBool.describe('Two pass'),
    preferredHwDevice: z.string().describe('Preferred hardware device'),
    transcode: TranscodePolicySchema,
    accel: TranscodeHardwareAccelerationSchema,
    accelDecode: configBool.describe('Accelerated decode'),
    tonemap: ToneMappingSchema,
    realtime: z
      .object({
        enabled: configBool.describe('Enable real-time HLS transcoding (alpha)'),
        videoCodecs: z.array(VideoCodecSchema).describe('Video codecs to use for real-time HLS transcoding'),
        resolutions: z.array(HlsVideoResolutionSchema).describe('Resolutions to use for real-time HLS transcoding'),
      })
      .meta({ id: 'SystemConfigFFmpegRealtimeDto' }),
  })
  .meta({ id: 'SystemConfigFFmpegDto' });

const SystemConfigJobSchema = z
  .object({
    thumbnailGeneration: JobSettingsSchema,
    metadataExtraction: JobSettingsSchema,
    videoConversion: JobSettingsSchema,
    faceDetection: JobSettingsSchema,
    smartSearch: JobSettingsSchema,
    backgroundTask: JobSettingsSchema,
    migration: JobSettingsSchema,
    search: JobSettingsSchema,
    sidecar: JobSettingsSchema,
    library: JobSettingsSchema,
    notifications: JobSettingsSchema,
    ocr: JobSettingsSchema,
    workflow: JobSettingsSchema,
    editor: JobSettingsSchema,
    integrityCheck: JobSettingsSchema,
  })
  .meta({ id: 'SystemConfigJobDto' });

const SystemConfigLibraryScanSchema = z
  .object({
    enabled: configBool.describe('Enabled'),
    cronExpression: cronExpressionSchema,
  })
  .meta({ id: 'SystemConfigLibraryScanDto' });

const SystemConfigLibraryWatchSchema = z
  .object({ enabled: configBool.describe('Enabled') })
  .meta({ id: 'SystemConfigLibraryWatchDto' });

const SystemConfigLibrarySchema = z
  .object({ scan: SystemConfigLibraryScanSchema, watch: SystemConfigLibraryWatchSchema })
  .meta({ id: 'SystemConfigLibraryDto' });

const SystemConfigLoggingSchema = z
  .object({
    enabled: configBool.describe('Enabled'),
    level: LogLevelSchema,
  })
  .meta({ id: 'SystemConfigLoggingDto' });

const MachineLearningAvailabilityChecksSchema = z
  .object({
    enabled: configBool.describe('Enabled'),
    timeout: z.int(),
    interval: z.int(),
  })
  .meta({ id: 'MachineLearningAvailabilityChecksDto' });

const SystemConfigMachineLearningSchema = z
  .object({
    enabled: configBool.describe('Enabled'),
    urls: z.array(z.string()).min(1).describe('ML service URLs'),
    availabilityChecks: MachineLearningAvailabilityChecksSchema,
    clip: CLIPConfigSchema,
    duplicateDetection: DuplicateDetectionConfigSchema,
    facialRecognition: FacialRecognitionConfigSchema,
    ocr: OcrConfigSchema,
  })
  .meta({ id: 'SystemConfigMachineLearningDto' });

const SystemConfigMapSchema = z
  .object({
    enabled: configBool.describe('Enabled'),
    lightStyle: z.url().describe('Light map style URL'),
    darkStyle: z.url().describe('Dark map style URL'),
  })
  .meta({ id: 'SystemConfigMapDto' });

export enum ReleaseChannel {
  Stable = 'stable',
  ReleaseCandidate = 'releaseCandidate',
}

const ReleaseChannelSchema = z.enum(ReleaseChannel).describe('Release channel').meta({ id: 'ReleaseChannel' });

const SystemConfigNewVersionCheckSchema = z
  .object({ enabled: configBool.describe('Enabled'), channel: ReleaseChannelSchema })
  .meta({ id: 'SystemConfigNewVersionCheckDto' });

const SystemConfigNightlyTasksSchema = z
  .object({
    startTime: z.iso
      .time({
        precision: -1,
        error: (iss) => `Invalid input: expected string in HH:MM format, received ${typeof iss.input}`,
      })
      .describe('Start time (HH:MM)'),
    databaseCleanup: configBool.describe('Database cleanup'),
    missingThumbnails: configBool.describe('Missing thumbnails'),
    clusterNewFaces: configBool.describe('Cluster new faces'),
    generateMemories: configBool.describe('Generate memories'),
    syncQuotaUsage: configBool.describe('Sync quota usage'),
  })
  .meta({ id: 'SystemConfigNightlyTasksDto' });

const SystemConfigOAuthSchema = z
  .object({
    autoLaunch: configBool.describe('Auto launch'),
    autoRegister: configBool.describe('Auto register'),
    buttonText: z.string().describe('Button text'),
    clientId: z.string().describe('Client ID'),
    clientSecret: z.string().describe('Client secret'),
    tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethodSchema,
    timeout: z.int().min(1).describe('Timeout'),
    allowInsecureRequests: configBool.describe('Allow insecure requests'),
    defaultStorageQuota: z.int().min(0).nullable().describe('Default storage quota'),
    enabled: configBool.describe('Enabled'),
    issuerUrl: z
      .string()
      .refine((url) => url.length === 0 || z.url().safeParse(url).success, {
        error: 'Issuer URL must be an empty string or a valid URL',
      })
      .describe('Issuer URL'),
    scope: z.string().describe('Scope'),
    prompt: z.string().describe('OAuth prompt parameter (e.g. select_account, login, consent)'),
    endSessionEndpoint: z
      .string()
      .refine((url) => url.length === 0 || z.url().safeParse(url).success, {
        error: 'endSessionEndpoint must be an empty string or a valid URL',
      })
      .describe('End session endpoint'),
    signingAlgorithm: z.string().describe('Signing algorithm'),
    profileSigningAlgorithm: z.string().describe('Profile signing algorithm'),
    storageLabelClaim: z.string().describe('Storage label claim'),
    storageQuotaClaim: z.string().describe('Storage quota claim'),
    roleClaim: z.string().describe('Role claim'),
    mobileOverrideEnabled: configBool.describe('Mobile override enabled'),
    mobileRedirectUri: z.string().describe('Mobile redirect URI (set to empty string to disable)'),
  })
  .transform((value, ctx) => {
    if (!value.mobileOverrideEnabled || value.mobileRedirectUri === '') {
      return value;
    }

    if (!z.url().safeParse(value.mobileRedirectUri).success) {
      ctx.issues.push({
        code: 'custom',
        message: 'Mobile redirect URI must be an empty string or a valid URL',
        input: value.mobileRedirectUri,
      });
      return z.NEVER;
    }

    return value;
  })
  .meta({
    id: 'SystemConfigOAuthDto',
  });

const SystemConfigPasswordLoginSchema = z
  .object({ enabled: configBool.describe('Enabled') })
  .meta({ id: 'SystemConfigPasswordLoginDto' });

const SystemConfigReverseGeocodingSchema = z
  .object({ enabled: configBool.describe('Enabled') })
  .meta({ id: 'SystemConfigReverseGeocodingDto' });

const SystemConfigFacesSchema = z
  .object({ import: configBool.describe('Import') })
  .meta({ id: 'SystemConfigFacesDto' });
const SystemConfigMetadataSchema = z.object({ faces: SystemConfigFacesSchema }).meta({ id: 'SystemConfigMetadataDto' });

const SystemConfigServerSchema = z
  .object({
    externalDomain: z
      .string()
      .refine((url) => url.length === 0 || z.url().safeParse(url).success, {
        error: 'External domain must be an empty string or a valid URL',
      })
      .describe('External domain'),
    loginPageMessage: z.string().describe('Login page message'),
    publicUsers: configBool.describe('Public users'),
  })
  .meta({ id: 'SystemConfigServerDto' });

const SystemConfigSmtpTransportSchema = z
  .object({
    ignoreCert: configBool.describe('Whether to ignore SSL certificate errors'),
    host: z.string().describe('SMTP server hostname'),
    port: z.int().min(0).max(65_535).describe('SMTP server port'),
    secure: configBool.describe('Whether to use secure connection (TLS/SSL)'),
    username: z.string().describe('SMTP username'),
    password: z.string().describe('SMTP password'),
  })
  .meta({ id: 'SystemConfigSmtpTransportDto' });

const SystemConfigSmtpSchema = z
  .object({
    enabled: configBool.describe('Whether SMTP email notifications are enabled'),
    from: z.string().describe('Email address to send from'),
    replyTo: z.string().describe('Email address for replies'),
    transport: SystemConfigSmtpTransportSchema,
  })
  .meta({ id: 'SystemConfigSmtpDto' });

const SystemConfigNotificationsSchema = z
  .object({ smtp: SystemConfigSmtpSchema })
  .meta({ id: 'SystemConfigNotificationsDto' });

const SystemConfigTemplateEmailsSchema = z
  .object({
    albumInviteTemplate: z.string().describe('Album invite template'),
    welcomeTemplate: z.string().describe('Welcome template'),
    albumUpdateTemplate: z.string().describe('Album update template'),
  })
  .meta({ id: 'SystemConfigTemplateEmailsDto' });
const SystemConfigTemplatesSchema = z
  .object({ email: SystemConfigTemplateEmailsSchema })
  .meta({ id: 'SystemConfigTemplatesDto' });

const SystemConfigStorageTemplateSchema = z
  .object({
    enabled: configBool.describe('Enabled'),
    hashVerificationEnabled: configBool.describe('Hash verification enabled'),
    template: z.string().describe('Template'),
  })
  .meta({ id: 'SystemConfigStorageTemplateDto' });

const SystemConfigTemplateStorageOptionSchema = z
  .object({
    yearOptions: z.array(z.string()).describe('Available year format options for storage template'),
    monthOptions: z.array(z.string()).describe('Available month format options for storage template'),
    weekOptions: z.array(z.string()).describe('Available week format options for storage template'),
    dayOptions: z.array(z.string()).describe('Available day format options for storage template'),
    hourOptions: z.array(z.string()).describe('Available hour format options for storage template'),
    minuteOptions: z.array(z.string()).describe('Available minute format options for storage template'),
    secondOptions: z.array(z.string()).describe('Available second format options for storage template'),
    presetOptions: z.array(z.string()).describe('Available preset template options'),
  })
  .meta({ id: 'SystemConfigTemplateStorageOptionDto' });

const SystemConfigThemeSchema = z
  .object({ customCss: z.string().describe('Custom CSS for theming') })
  .meta({ id: 'SystemConfigThemeDto' });

const SystemConfigGeneratedImageSchema = z
  .object({
    format: ImageFormatSchema,
    quality: z.int().min(1).max(100).describe('Quality'),
    size: z.int().min(1).describe('Size'),
    progressive: configBool.default(false).optional().describe('Progressive'),
  })
  .meta({ id: 'SystemConfigGeneratedImageDto' });

const SystemConfigGeneratedFullsizeImageSchema = z
  .object({
    enabled: configBool.describe('Enabled'),
    format: ImageFormatSchema,
    quality: z.int().min(1).max(100).describe('Quality'),
    progressive: configBool.default(false).optional().describe('Progressive'),
  })
  .meta({ id: 'SystemConfigGeneratedFullsizeImageDto' });

const SystemConfigImageSchema = z
  .object({
    thumbnail: SystemConfigGeneratedImageSchema,
    preview: SystemConfigGeneratedImageSchema,
    fullsize: SystemConfigGeneratedFullsizeImageSchema,
    colorspace: ColorspaceSchema,
    extractEmbedded: configBool.describe('Extract embedded'),
  })
  .meta({ id: 'SystemConfigImageDto' });

const SystemConfigTrashSchema = z
  .object({
    enabled: configBool.describe('Enabled'),
    days: z.int().min(0).describe('Days'),
  })
  .meta({ id: 'SystemConfigTrashDto' });

const SystemConfigUserSchema = z
  .object({
    deleteDelay: z.int().min(1).describe('Delete delay'),
  })
  .meta({ id: 'SystemConfigUserDto' });

export const SystemConfigSchema = z
  .object({
    backup: SystemConfigBackupsSchema,
    ffmpeg: SystemConfigFFmpegSchema,
    logging: SystemConfigLoggingSchema,
    machineLearning: SystemConfigMachineLearningSchema,
    map: SystemConfigMapSchema,
    newVersionCheck: SystemConfigNewVersionCheckSchema,
    nightlyTasks: SystemConfigNightlyTasksSchema,
    oauth: SystemConfigOAuthSchema,
    passwordLogin: SystemConfigPasswordLoginSchema,
    reverseGeocoding: SystemConfigReverseGeocodingSchema,
    metadata: SystemConfigMetadataSchema,
    storageTemplate: SystemConfigStorageTemplateSchema,
    job: SystemConfigJobSchema,
    image: SystemConfigImageSchema,
    trash: SystemConfigTrashSchema,
    theme: SystemConfigThemeSchema,
    library: SystemConfigLibrarySchema,
    notifications: SystemConfigNotificationsSchema,
    templates: SystemConfigTemplatesSchema,
    server: SystemConfigServerSchema,
    user: SystemConfigUserSchema,
    integrityChecks: SystemConfigIntegrityChecksSchema,
  })
  .describe('System configuration')
  .meta({ id: 'SystemConfigDto' });

export class SystemConfigFFmpegDto extends createZodDto(SystemConfigFFmpegSchema) {}
export class SystemConfigSmtpDto extends createZodDto(SystemConfigSmtpSchema) {}
export class SystemConfigTemplateStorageOptionDto extends createZodDto(SystemConfigTemplateStorageOptionSchema) {}
export class SystemConfigDto extends createZodDto(SystemConfigSchema) {}

export function mapConfig(config: SystemConfig): SystemConfigDto {
  return config;
}
