import { BadRequestException, Injectable, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Prisma } from '@prisma/client';
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto';
import { PrismaService } from '../database/prisma.service';
import { INTEGRATION_TEMPLATES, IntegrationCategory, integrationTemplate, normalizeIntegrationProvider } from './integration-config.types';

type CachedIntegrationConfig = {
  category: string;
  provider: string;
  displayName: string;
  enabled: boolean;
  publicConfig: Record<string, unknown>;
  secretConfig: Record<string, string>;
  updatedById?: string | null;
  updatedAt?: Date;
};

type UpdateIntegrationConfigInput = {
  category?: string;
  displayName?: string;
  enabled?: boolean;
  publicConfig?: Record<string, unknown>;
  secretConfig?: Record<string, unknown>;
  clearPublicKeys?: string[];
  clearSecretKeys?: string[];
};

@Injectable()
export class IntegrationConfigService implements OnModuleInit {
  private readonly cache = new Map<string, CachedIntegrationConfig>();

  constructor(
    private readonly prisma: PrismaService,
    private readonly config: ConfigService
  ) {}

  async onModuleInit() {
    await this.refreshCache();
  }

  async refreshCache() {
    const configs = await this.prisma.integrationConfig.findMany();
    this.cache.clear();
    configs.forEach((config) => {
      this.cache.set(config.provider, {
        category: config.category,
        provider: config.provider,
        displayName: config.displayName,
        enabled: config.enabled,
        publicConfig: this.jsonObject(config.publicConfig),
        secretConfig: this.stringMap(config.secretConfig),
        updatedById: config.updatedById,
        updatedAt: config.updatedAt
      });
    });
  }

  listTemplates() {
    return INTEGRATION_TEMPLATES;
  }

  listConfigs() {
    const providers = new Set([...INTEGRATION_TEMPLATES.map((template) => template.provider), ...this.cache.keys()]);
    return Array.from(providers)
      .sort()
      .map((provider) => this.describeProvider(provider));
  }

  getConfig(provider: string) {
    return this.describeProvider(normalizeIntegrationProvider(provider));
  }

  async updateConfig(provider: string, input: UpdateIntegrationConfigInput, actorId: string) {
    const normalizedProvider = normalizeIntegrationProvider(provider);
    const template = integrationTemplate(normalizedProvider);
    const existing = this.cache.get(normalizedProvider);
    const category = this.normalizeCategory(input.category ?? existing?.category ?? template?.category);
    if (!category) {
      throw new BadRequestException('category is required for custom integration providers.');
    }

    const publicConfig = {
      ...(existing?.publicConfig ?? {}),
      ...this.cleanPublicConfig(input.publicConfig)
    };
    for (const key of input.clearPublicKeys ?? []) {
      delete publicConfig[key];
    }

    const secretConfig = {
      ...(existing?.secretConfig ?? {}),
      ...this.encryptSecretConfig(input.secretConfig)
    };
    for (const key of input.clearSecretKeys ?? []) {
      delete secretConfig[key];
    }

    const saved = await this.prisma.integrationConfig.upsert({
      where: { provider: normalizedProvider },
      create: {
        provider: normalizedProvider,
        category,
        displayName: input.displayName ?? template?.displayName ?? normalizedProvider,
        enabled: input.enabled ?? true,
        publicConfig: this.toJson(publicConfig),
        secretConfig: this.toJson(secretConfig),
        updatedById: actorId
      },
      update: {
        category,
        displayName: input.displayName ?? existing?.displayName ?? template?.displayName ?? normalizedProvider,
        enabled: input.enabled,
        publicConfig: this.toJson(publicConfig),
        secretConfig: this.toJson(secretConfig),
        updatedById: actorId
      }
    });

    this.cache.set(saved.provider, {
      category: saved.category,
      provider: saved.provider,
      displayName: saved.displayName,
      enabled: saved.enabled,
      publicConfig: this.jsonObject(saved.publicConfig),
      secretConfig: this.stringMap(saved.secretConfig),
      updatedById: saved.updatedById,
      updatedAt: saved.updatedAt
    });

    return this.describeProvider(saved.provider);
  }

  getValue(provider: string, key: string, envKey?: string, fallback = ''): string {
    return this.cachedPublicValue(provider, key) ?? this.cachedSecretValue(provider, key) ?? (envKey ? this.config.get<string>(envKey) : undefined) ?? fallback;
  }

  getPublicValue(provider: string, key: string, envKey?: string): string | undefined {
    const value = this.cachedPublicValue(provider, key);
    if (value !== undefined && value !== null && value !== '') {
      return value;
    }
    return envKey ? this.config.get<string>(envKey) : undefined;
  }

  getSecretValue(provider: string, key: string, envKey?: string): string | undefined {
    const value = this.cachedSecretValue(provider, key);
    if (value !== undefined && value !== null && value !== '') {
      return value;
    }
    return envKey ? this.config.get<string>(envKey) : undefined;
  }

  isEnabled(provider: string) {
    return this.cache.get(normalizeIntegrationProvider(provider))?.enabled ?? true;
  }

  hasUsableValue(value?: string | null) {
    return Boolean(value && !/REPLACE_ME|YOUR_|your-|placeholder/i.test(value));
  }

  private describeProvider(provider: string) {
    const normalizedProvider = normalizeIntegrationProvider(provider);
    const template = integrationTemplate(normalizedProvider);
    const stored = this.cache.get(normalizedProvider);
    const publicConfig = stored?.publicConfig ?? {};
    const secretConfig = stored?.secretConfig ?? {};
    const secretKeys = Array.from(new Set([...(template?.secretKeys ?? []), ...Object.keys(secretConfig)])).sort();

    return {
      category: stored?.category ?? template?.category ?? 'CUSTOM',
      provider: normalizedProvider,
      displayName: stored?.displayName ?? template?.displayName ?? normalizedProvider,
      enabled: stored?.enabled ?? true,
      configured: secretKeys.length
        ? secretKeys.some((key) => this.hasUsableValue(this.decryptOptional(secretConfig[key])))
        : Object.keys(publicConfig).length > 0,
      publicConfig,
      secrets: Object.fromEntries(
        secretKeys.map((key) => {
          const value = this.decryptOptional(secretConfig[key]);
          return [key, { configured: this.hasUsableValue(value), masked: value ? this.maskSecret(value) : null }];
        })
      ),
      template: template
        ? {
            publicKeys: template.publicKeys,
            secretKeys: template.secretKeys,
            description: template.description
          }
        : null,
      updatedById: stored?.updatedById ?? null,
      updatedAt: stored?.updatedAt ?? null
    };
  }

  private cachedPublicValue(provider: string, key: string): string | undefined {
    const value = this.cache.get(normalizeIntegrationProvider(provider))?.publicConfig[key];
    return value !== undefined && value !== null && value !== '' ? String(value) : undefined;
  }

  private cachedSecretValue(provider: string, key: string): string | undefined {
    const encrypted = this.cache.get(normalizeIntegrationProvider(provider))?.secretConfig[key];
    return encrypted ? this.decrypt(encrypted) : undefined;
  }

  private normalizeCategory(value?: string): IntegrationCategory | undefined {
    if (!value) {
      return undefined;
    }
    const normalized = value.trim().toUpperCase();
    if (['PAYMENT', 'SMS', 'EMAIL', 'CUSTOM'].includes(normalized)) {
      return normalized as IntegrationCategory;
    }
    throw new BadRequestException('category must be PAYMENT, SMS, EMAIL, or CUSTOM.');
  }

  private cleanPublicConfig(config?: Record<string, unknown>) {
    if (!config) {
      return {};
    }
    return Object.fromEntries(
      Object.entries(config).filter(([, value]) => value !== undefined && value !== null && value !== '')
    );
  }

  private encryptSecretConfig(config?: Record<string, unknown>) {
    if (!config) {
      return {};
    }
    return Object.fromEntries(
      Object.entries(config)
        .filter(([, value]) => value !== undefined && value !== null && value !== '')
        .map(([key, value]) => [key, this.encrypt(String(value))])
    );
  }

  private encrypt(value: string) {
    const iv = randomBytes(12);
    const cipher = createCipheriv('aes-256-gcm', this.encryptionKey(), iv);
    const encrypted = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]);
    const tag = cipher.getAuthTag();
    return `v1:${iv.toString('base64')}:${tag.toString('base64')}:${encrypted.toString('base64')}`;
  }

  private decrypt(value: string) {
    if (!value.startsWith('v1:')) {
      return value;
    }
    const [, ivRaw, tagRaw, encryptedRaw] = value.split(':');
    const decipher = createDecipheriv('aes-256-gcm', this.encryptionKey(), Buffer.from(ivRaw, 'base64'));
    decipher.setAuthTag(Buffer.from(tagRaw, 'base64'));
    return Buffer.concat([decipher.update(Buffer.from(encryptedRaw, 'base64')), decipher.final()]).toString('utf8');
  }

  private decryptOptional(value?: string) {
    if (!value) {
      return undefined;
    }
    try {
      return this.decrypt(value);
    } catch {
      return undefined;
    }
  }

  private encryptionKey() {
    const secret =
      this.config.get<string>('INTEGRATION_CONFIG_SECRET') ??
      this.config.get<string>('JWT_REFRESH_SECRET') ??
      this.config.get<string>('JWT_ACCESS_SECRET') ??
      'local-integration-config-secret';
    return createHash('sha256').update(secret).digest();
  }

  private maskSecret(value: string) {
    if (value.length <= 4) {
      return '****';
    }
    return `****${value.slice(-4)}`;
  }

  private jsonObject(value: Prisma.JsonValue): Record<string, unknown> {
    return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
  }

  private stringMap(value: Prisma.JsonValue): Record<string, string> {
    const object = this.jsonObject(value);
    return Object.fromEntries(Object.entries(object).map(([key, item]) => [key, String(item)]));
  }

  private toJson(value: Record<string, unknown>): Prisma.InputJsonValue {
    return JSON.parse(JSON.stringify(value)) as Prisma.InputJsonValue;
  }
}
