import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { EncryptionService } from '../../common/services/encryption.service';
import { SystemSetting } from '@prisma/client';

interface CacheEntry {
  value: string;
  expiresAt: number;
}

export interface UpsertData {
  value: string;
  category: string;
  description?: string;
  valueType?: string;
  isSecret?: boolean;
}

export interface BulkUpsertItem extends UpsertData {
  key: string;
}

export interface SettingResponse extends Omit<SystemSetting, 'value'> {
  value: string;
}

/**
 * Service for managing system-wide settings stored in the database.
 *
 * Features:
 * - In-memory cache with 60s TTL to reduce database reads
 * - AES-256-GCM encryption for secrets via EncryptionService
 * - Type-safe value parsing for number, boolean, json, and string types
 * - Idempotent upsert operations (safe to call multiple times)
 */
@Injectable()
export class SystemSettingsService {
  private readonly logger = new Logger(SystemSettingsService.name);
  private readonly cache = new Map<string, CacheEntry>();
  private readonly CACHE_TTL_MS = 60_000; // 60 seconds
  private readonly SECRET_MASK = '••••••••';

  constructor(
    private readonly prisma: PrismaService,
    private readonly encryptionService: EncryptionService,
  ) {}

  /**
   * Returns all settings, grouped by category.
   * Secret values are masked in the response.
   */
  async findAll(category?: string): Promise<{ settings: SettingResponse[]; categories: string[] }> {
    const where = category ? { category } : undefined;

    const settings = await this.prisma.systemSetting.findMany({
      where,
      orderBy: [{ category: 'asc' }, { key: 'asc' }],
    });

    const masked = settings.map((s) => this.maskSecret(s));

    const categories = await this.prisma.systemSetting
      .findMany({ distinct: ['category'], select: { category: true }, orderBy: { category: 'asc' } })
      .then((rows) => rows.map((r) => r.category));

    return { settings: masked, categories };
  }

  /**
   * Returns a single setting by key. Secret values are masked in the response.
   */
  async findByKey(key: string): Promise<SettingResponse> {
    const setting = await this.prisma.systemSetting.findUnique({ where: { key } });

    if (!setting) {
      throw new NotFoundException(`Setting with key "${key}" not found`);
    }

    return this.maskSecret(setting);
  }

  /**
   * Internal method: returns the actual decrypted value for use by other services.
   * Uses in-memory cache to avoid repeated database reads.
   */
  async getValue(key: string): Promise<string | null> {
    const now = Date.now();
    const cached = this.cache.get(key);

    if (cached && cached.expiresAt > now) {
      return cached.value;
    }

    const setting = await this.prisma.systemSetting.findUnique({ where: { key } });

    if (!setting) {
      return null;
    }

    const value = setting.isSecret
      ? this.encryptionService.decrypt(setting.value)
      : setting.value;

    this.cache.set(key, { value, expiresAt: now + this.CACHE_TTL_MS });

    return value;
  }

  /**
   * Returns a typed value parsed according to the setting's valueType.
   * Falls back to defaultValue when the key is not found or parsing fails.
   */
  async getTypedValue<T>(key: string, defaultValue: T): Promise<T> {
    const raw = await this.getValue(key);

    if (raw === null) {
      return defaultValue;
    }

    const setting = await this.prisma.systemSetting.findUnique({
      where: { key },
      select: { valueType: true },
    });

    const valueType = setting?.valueType ?? 'string';

    try {
      switch (valueType) {
        case 'number':
          return Number(raw) as unknown as T;
        case 'boolean':
          return (raw === 'true' || raw === '1') as unknown as T;
        case 'json':
          return JSON.parse(raw) as T;
        default:
          return raw as unknown as T;
      }
    } catch (error) {
      this.logger.warn(`Failed to parse setting "${key}" as ${valueType}, using default value`);
      return defaultValue;
    }
  }

  /**
   * Creates or updates a setting. Encrypts the value if isSecret is true.
   * Invalidates the cache entry for the key.
   */
  async upsert(key: string, data: UpsertData): Promise<SettingResponse> {
    const storedValue = data.isSecret
      ? this.encryptionService.encrypt(data.value)
      : data.value;

    const setting = await this.prisma.systemSetting.upsert({
      where: { key },
      create: {
        key,
        value: storedValue,
        category: data.category,
        description: data.description,
        valueType: data.valueType ?? 'string',
        isSecret: data.isSecret ?? false,
      },
      update: {
        value: storedValue,
        category: data.category,
        description: data.description,
        ...(data.valueType !== undefined && { valueType: data.valueType }),
        ...(data.isSecret !== undefined && { isSecret: data.isSecret }),
      },
    });

    this.invalidateCache(key);

    return this.maskSecret(setting);
  }

  /**
   * Upserts multiple settings in a single database transaction.
   */
  async bulkUpsert(settings: BulkUpsertItem[]): Promise<SettingResponse[]> {
    const results: SettingResponse[] = [];

    await this.prisma.$transaction(async (tx) => {
      for (const item of settings) {
        const storedValue = item.isSecret
          ? this.encryptionService.encrypt(item.value)
          : item.value;

        const setting = await tx.systemSetting.upsert({
          where: { key: item.key },
          create: {
            key: item.key,
            value: storedValue,
            category: item.category ?? 'general',
            description: item.description,
            valueType: item.valueType ?? 'string',
            isSecret: item.isSecret ?? false,
          },
          update: {
            value: storedValue,
            category: item.category ?? 'general',
            description: item.description,
            ...(item.valueType !== undefined && { valueType: item.valueType }),
            ...(item.isSecret !== undefined && { isSecret: item.isSecret }),
          },
        });

        results.push(this.maskSecret(setting));
      }
    });

    // Invalidate all affected cache entries
    settings.forEach((s) => this.invalidateCache(s.key));

    return results;
  }

  /**
   * Seeds default system settings. Uses upsert so it is idempotent
   * and safe to call on every application startup or in the seed script.
   */
  async seedDefaults(): Promise<void> {
    const defaultSettings: BulkUpsertItem[] = [
      // Sync intervals
      { key: 'sync.interval.plentyone', value: '15', category: 'sync', valueType: 'number', description: 'PlentyONE Sync-Intervall (Minuten)' },
      { key: 'sync.interval.zulip', value: '5', category: 'sync', valueType: 'number', description: 'Zulip Sync-Intervall (Minuten)' },
      { key: 'sync.interval.todoist', value: '10', category: 'sync', valueType: 'number', description: 'Todoist Sync-Intervall (Minuten)' },
      { key: 'sync.interval.freescout', value: '10', category: 'sync', valueType: 'number', description: 'FreeScout Sync-Intervall (Minuten)' },
      { key: 'sync.interval.nextcloud', value: '30', category: 'sync', valueType: 'number', description: 'Nextcloud Sync-Intervall (Minuten)' },
      { key: 'sync.interval.ecodms', value: '60', category: 'sync', valueType: 'number', description: 'ecoDMS Sync-Intervall (Minuten)' },
      { key: 'sync.interval.gembadocs', value: '30', category: 'sync', valueType: 'number', description: 'GembaDocs Sync-Intervall (Minuten)' },
      // Feature flags
      { key: 'feature.syncJobs.enabled', value: 'false', category: 'feature', valueType: 'boolean', description: 'Hintergrund-Sync-Jobs aktivieren' },
      { key: 'feature.swagger.enabled', value: 'true', category: 'feature', valueType: 'boolean', description: 'Swagger API-Dokumentation aktivieren' },
      // CORS
      { key: 'cors.origins', value: 'http://localhost:3000', category: 'cors', valueType: 'string', description: 'Erlaubte CORS Origins (kommagetrennt)' },
      // Branding
      { key: 'branding.appName', value: 'tOS', category: 'branding', valueType: 'string', description: 'Anwendungsname' },
      { key: 'branding.companyName', value: '', category: 'branding', valueType: 'string', description: 'Firmenname' },
      { key: 'branding.logoUrl', value: '', category: 'branding', valueType: 'string', description: 'Logo-URL' },
    ];

    await this.bulkUpsert(defaultSettings);
    this.logger.log(`Seeded ${defaultSettings.length} default system settings`);
  }

  /**
   * Clears the in-memory cache. Pass a key to clear only that entry,
   * or call without arguments to clear the entire cache.
   */
  invalidateCache(key?: string): void {
    if (key) {
      this.cache.delete(key);
    } else {
      this.cache.clear();
    }
  }

  // ---------------------------------------------------------------------------
  // Private helpers
  // ---------------------------------------------------------------------------

  private maskSecret(setting: SystemSetting): SettingResponse {
    return {
      ...setting,
      value: setting.isSecret ? this.SECRET_MASK : setting.value,
    };
  }
}
