import {
  Controller,
  Get,
  Post,
  Delete,
  Body,
  Query,
  Logger,
  UseGuards,
  BadRequestException,
} from '@nestjs/common';
import { ApiTags, ApiOperation } from '@nestjs/swagger';
import { Roles } from '../../common/decorators/roles.decorator';
import { RolesGuard } from '../../common/guards/roles.guard';
import { randomUUID, X509Certificate, createPrivateKey } from 'crypto';
import { promises as dns } from 'dns';
import { writeFile, readFile, unlink, access } from 'fs/promises';
import { join } from 'path';
import { RedisService } from '../../redis/redis.service';

/**
 * Ein externer Link fuer die Sidebar-Navigation.
 * Icons werden im Frontend automatisch als Favicon der URL geladen.
 */
interface ExternalLink {
  id: string;
  label: string;
  url: string;
  sortOrder: number;
  customIcon?: string; // Optional: Base64-encoded custom icon
}

const EXTERNAL_LINKS_KEY = 'platform_external_links';
const BRANDING_LOGO_KEY = 'platform_branding_logo';
const SSL_CONFIG_KEY = 'platform_ssl_config';

/**
 * SSL/Domain-Konfiguration — Metadata in Redis, Cert-Dateien auf Filesystem.
 */
interface SslCertInfo {
  subject: string;
  issuer: string;
  validFrom: string;
  validTo: string;
  sanList: string[];
  dnsMatch: boolean;
  isExpired: boolean;
  fingerprint: string;
}

interface SslConfig {
  enabled: boolean;
  dnsName: string | null;
  certificate: SslCertInfo | null;
  lastUpdated: string | null;
}

interface SslSaveRequest {
  dnsName: string;
  certificate: string;
  privateKey: string;
  enabled: boolean;
}

interface DnsCheckResponse {
  resolves: boolean;
  addresses: string[];
  error?: string;
}

@ApiTags('Settings')
@Controller('settings')
export class SettingsController {
  private readonly logger = new Logger(SettingsController.name);

  constructor(private readonly redis: RedisService) {}

  /**
   * GET /api/v1/settings/external-links
   * Externe Links fuer die Sidebar lesen (jeder authentifizierte User).
   */
  @Get('external-links')
  @ApiOperation({ summary: 'Externe Links lesen' })
  async getExternalLinks(): Promise<ExternalLink[]> {
    const raw = await this.redis.get(EXTERNAL_LINKS_KEY);
    if (!raw) return [];

    try {
      return JSON.parse(raw) as ExternalLink[];
    } catch {
      return [];
    }
  }

  /**
   * POST /api/v1/settings/external-links
   * Externe Links speichern (nur PLATFORM_ADMIN).
   * Erwartet ein Array von Links.
   */
  @Post('external-links')
  @Roles('PLATFORM_ADMIN')
  @UseGuards(RolesGuard)
  @ApiOperation({ summary: 'Externe Links speichern (Admin)' })
  async saveExternalLinks(
    @Body() body: { links: ExternalLink[] },
  ): Promise<{ success: boolean; count: number }> {
    if (!Array.isArray(body.links)) {
      throw new BadRequestException('links muss ein Array sein');
    }

    // Validierung
    for (const link of body.links) {
      if (!link.label?.trim()) {
        throw new BadRequestException('Jeder Link braucht ein Label');
      }
      if (!link.url?.trim()) {
        throw new BadRequestException('Jeder Link braucht eine URL');
      }
    }

    // Sortierung sicherstellen
    const sorted = body.links.map((link, index) => ({
      id: link.id || randomUUID(),
      label: link.label.trim(),
      url: link.url.trim(),
      sortOrder: link.sortOrder ?? index,
      ...(link.customIcon && { customIcon: link.customIcon }),
    }));

    sorted.sort((a, b) => a.sortOrder - b.sortOrder);

    await this.redis.set(EXTERNAL_LINKS_KEY, JSON.stringify(sorted));

    this.logger.log(`${sorted.length} externe Links gespeichert`);

    return { success: true, count: sorted.length };
  }

  /**
   * GET /api/v1/settings/branding
   * Branding-Einstellungen lesen (Logo, Sidebar-Farbe, Login-Hintergrund etc.).
   */
  @Get('branding')
  @ApiOperation({ summary: 'Branding-Einstellungen lesen' })
  async getBranding(): Promise<{
    logo: string | null;
    sidebarColor: string | null;
    logoWidth: number | null;
    sidebarWidth: number | null;
    loginBgType: 'gradient' | 'solid' | 'image' | null;
    loginBgColor1: string | null;
    loginBgColor2: string | null;
    loginBgImage: string | null;
  }> {
    const raw = await this.redis.get(BRANDING_LOGO_KEY);
    const empty = {
      logo: null, sidebarColor: null, logoWidth: null, sidebarWidth: null,
      loginBgType: null, loginBgColor1: null, loginBgColor2: null, loginBgImage: null,
    };
    if (!raw) return empty;

    try {
      const data = JSON.parse(raw);
      const loginBgType = ['gradient', 'solid', 'image'].includes(data.loginBgType)
        ? (data.loginBgType as 'gradient' | 'solid' | 'image')
        : null;
      return {
        logo:          data.logo || null,
        sidebarColor:  data.sidebarColor || null,
        logoWidth:     typeof data.logoWidth === 'number' ? data.logoWidth : null,
        sidebarWidth:  typeof data.sidebarWidth === 'number' ? data.sidebarWidth : null,
        loginBgType,
        loginBgColor1: data.loginBgColor1 || null,
        loginBgColor2: data.loginBgColor2 || null,
        loginBgImage:  data.loginBgImage || null,
      };
    } catch {
      // Legacy: nur Logo als String
      return { ...empty, logo: raw };
    }
  }

  /**
   * POST /api/v1/settings/branding
   * Branding-Einstellungen speichern (nur PLATFORM_ADMIN).
   */
  @Post('branding')
  @Roles('PLATFORM_ADMIN')
  @UseGuards(RolesGuard)
  @ApiOperation({ summary: 'Branding-Einstellungen speichern (Admin)' })
  async saveBranding(
    @Body() body: {
      logo?: string | null;
      sidebarColor?: string | null;
      logoWidth?: number | null;
      sidebarWidth?: number | null;
      loginBgType?: string | null;
      loginBgColor1?: string | null;
      loginBgColor2?: string | null;
      loginBgImage?: string | null;
    },
  ): Promise<{ success: boolean }> {
    if (body.logo && body.logo.length > 500_000) {
      throw new BadRequestException('Logo darf maximal 500KB gross sein');
    }

    if (body.loginBgImage && body.loginBgImage.length > 2_000_000) {
      throw new BadRequestException('Hintergrundbild darf maximal 2MB gross sein');
    }

    const validBgTypes = ['gradient', 'solid', 'image'];
    const loginBgType = body.loginBgType && validBgTypes.includes(body.loginBgType)
      ? body.loginBgType
      : null;

    // Werte-Grenzen
    const logoWidth = typeof body.logoWidth === 'number'
      ? Math.min(Math.max(Math.round(body.logoWidth), 40), 240)
      : null;
    const sidebarWidth = typeof body.sidebarWidth === 'number'
      ? Math.min(Math.max(Math.round(body.sidebarWidth), 200), 360)
      : null;

    const data = {
      logo:          body.logo || null,
      sidebarColor:  body.sidebarColor || null,
      logoWidth,
      sidebarWidth,
      loginBgType,
      loginBgColor1: body.loginBgColor1 || null,
      loginBgColor2: body.loginBgColor2 || null,
      loginBgImage:  body.loginBgImage || null,
    };

    await this.redis.set(BRANDING_LOGO_KEY, JSON.stringify(data));

    this.logger.log('Branding aktualisiert');
    return { success: true };
  }

  /**
   * GET /api/v1/settings/favicon?url=https://example.com
   * Favicon-URL fuer eine beliebige Webseite ermitteln.
   * Parst die HTML-Seite nach <link rel="icon"> Tags und cached das Ergebnis.
   */
  @Get('favicon')
  @ApiOperation({ summary: 'Favicon-URL fuer eine Webseite ermitteln' })
  async getFavicon(
    @Query('url') url: string,
  ): Promise<{ faviconUrl: string | null }> {
    if (!url) {
      throw new BadRequestException('url Parameter fehlt');
    }

    // Cache pruefen (24h)
    const cacheKey = `favicon:${url}`;
    const cached = await this.redis.get(cacheKey);
    if (cached !== null) {
      return { faviconUrl: cached || null };
    }

    const faviconUrl = await this.discoverFavicon(url);

    // In Redis cachen (24h), auch leeres Ergebnis cachen
    await this.redis.set(cacheKey, faviconUrl || '', 86400);

    return { faviconUrl };
  }

  /**
   * Versucht das Favicon einer Webseite zu finden:
   * 1. HTML der Seite laden und <link rel="icon"> parsen
   * 2. Fallback: /favicon.ico pruefen
   */
  private async discoverFavicon(urlStr: string): Promise<string | null> {
    try {
      const parsed = new URL(urlStr);
      const origin = parsed.origin;

      // 1. HTML laden und <link rel="icon"> suchen
      try {
        const controller = new AbortController();
        const timeout = setTimeout(() => controller.abort(), 5000);

        const res = await fetch(urlStr, {
          signal: controller.signal,
          headers: {
            'User-Agent': 'Mozilla/5.0 (compatible; INSIGHT/1.0)',
            Accept: 'text/html',
          },
          redirect: 'follow',
        });
        clearTimeout(timeout);

        if (res.ok) {
          const html = await res.text();
          const iconUrl = this.parseFaviconFromHtml(html, origin);
          if (iconUrl) {
            return iconUrl;
          }
        }
      } catch (e) {
        this.logger.debug(`HTML fetch fehlgeschlagen fuer ${urlStr}: ${e}`);
      }

      // 2. Fallback: /favicon.ico direkt pruefen
      try {
        const controller = new AbortController();
        const timeout = setTimeout(() => controller.abort(), 3000);

        const icoUrl = `${origin}/favicon.ico`;
        const res = await fetch(icoUrl, {
          method: 'HEAD',
          signal: controller.signal,
          redirect: 'follow',
        });
        clearTimeout(timeout);

        if (res.ok) {
          const contentType = res.headers.get('content-type') || '';
          if (
            contentType.includes('image') ||
            contentType.includes('icon') ||
            contentType.includes('octet-stream')
          ) {
            return icoUrl;
          }
        }
      } catch (e) {
        this.logger.debug(`favicon.ico check fehlgeschlagen fuer ${origin}: ${e}`);
      }

      return null;
    } catch {
      return null;
    }
  }

  /**
   * Parst HTML nach <link rel="icon"> oder <link rel="shortcut icon"> Tags.
   */
  private parseFaviconFromHtml(html: string, origin: string): string | null {
    // Nur den <head> Bereich betrachten (Performance)
    const headMatch = html.match(/<head[\s>]([\s\S]*?)<\/head>/i);
    const headHtml = headMatch ? headMatch[1] : html.slice(0, 10000);

    // Alle <link> Tags mit rel="icon" oder rel="shortcut icon" finden
    const linkRegex =
      /<link\s[^>]*rel\s*=\s*["'](?:shortcut\s+)?icon["'][^>]*>/gi;
    const matches = headHtml.match(linkRegex);
    if (!matches || matches.length === 0) return null;

    // href aus dem besten Match extrahieren
    // Bevorzuge groessere Icons (sizes Attribut)
    let bestHref: string | null = null;
    let bestSize = 0;

    for (const tag of matches) {
      const hrefMatch = tag.match(/href\s*=\s*["']([^"']+)["']/i);
      if (!hrefMatch) continue;

      const href = hrefMatch[1];

      // Groesse parsen
      const sizeMatch = tag.match(/sizes\s*=\s*["'](\d+)x\d+["']/i);
      const size = sizeMatch ? parseInt(sizeMatch[1], 10) : 16;

      if (!bestHref || size > bestSize) {
        bestHref = href;
        bestSize = size;
      }
    }

    if (!bestHref) return null;

    // Relative URLs aufloesen
    if (bestHref.startsWith('//')) {
      return `https:${bestHref}`;
    }
    if (bestHref.startsWith('/')) {
      return `${origin}${bestHref}`;
    }
    if (bestHref.startsWith('http')) {
      return bestHref;
    }
    return `${origin}/${bestHref}`;
  }

  // ============================================================
  //  SSL / Domain Configuration
  // ============================================================

  private get certsPath(): string {
    return process.env.TRAEFIK_CERTS_PATH || '/app/traefik-certs';
  }

  private get dynamicConfigPath(): string {
    return process.env.TRAEFIK_DYNAMIC_PATH || '/app/traefik-dynamic';
  }

  /**
   * GET /api/v1/settings/ssl
   * Aktuelle SSL/Domain-Konfiguration lesen.
   */
  @Get('ssl')
  @Roles('PLATFORM_ADMIN')
  @UseGuards(RolesGuard)
  @ApiOperation({ summary: 'SSL/Domain-Konfiguration lesen' })
  async getSslConfig(): Promise<SslConfig> {
    const raw = await this.redis.get(SSL_CONFIG_KEY);
    if (!raw) {
      return { enabled: false, dnsName: null, certificate: null, lastUpdated: null };
    }

    try {
      return JSON.parse(raw) as SslConfig;
    } catch {
      return { enabled: false, dnsName: null, certificate: null, lastUpdated: null };
    }
  }

  /**
   * POST /api/v1/settings/ssl
   * SSL-Konfiguration speichern: DNS-Name + Zertifikat + Key.
   * Schreibt Cert-Dateien und generiert Traefik Dynamic Config.
   */
  @Post('ssl')
  @Roles('PLATFORM_ADMIN')
  @UseGuards(RolesGuard)
  @ApiOperation({ summary: 'SSL/Domain-Konfiguration speichern (Admin)' })
  async saveSslConfig(
    @Body() body: SslSaveRequest,
  ): Promise<{ success: boolean; config: SslConfig; warnings: string[] }> {
    const warnings: string[] = [];

    // 1. DNS-Name validieren
    const dnsName = body.dnsName?.trim();
    if (!dnsName) {
      throw new BadRequestException('DNS-Name ist erforderlich');
    }
    if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$/.test(dnsName)) {
      throw new BadRequestException('Ungueltiger DNS-Name. Beispiel: insight.firma.de');
    }

    // 2. DNS-Aufloesung pruefen (Warnung, kein Fehler)
    try {
      await dns.resolve4(dnsName);
    } catch {
      warnings.push(`DNS-Name "${dnsName}" konnte nicht aufgeloest werden. Stellen Sie sicher, dass ein A-Record auf den Server zeigt.`);
    }

    // 3. PEM-Zertifikat — Sentinel __KEEP_EXISTING__ erlaubt Update ohne neuen Cert-Upload
    const keepExisting = body.certificate?.trim() === '__KEEP_EXISTING__' && body.privateKey?.trim() === '__KEEP_EXISTING__';
    let certPem: string | undefined;
    let keyPem: string | undefined;

    if (keepExisting) {
      // Bestehende Dateien verwenden — pruefen ob sie existieren
      try {
        certPem = await readFile(join(this.certsPath, 'custom.crt'), 'utf-8');
        keyPem = await readFile(join(this.certsPath, 'custom.key'), 'utf-8');
      } catch {
        throw new BadRequestException('Kein bestehendes Zertifikat vorhanden. Bitte Zertifikat und Key hochladen.');
      }
    } else {
      // Neues Zertifikat validieren
      certPem = body.certificate?.trim();
      if (!certPem) {
        throw new BadRequestException('SSL-Zertifikat ist erforderlich');
      }
      // Base64 Data-URL decodieren falls noetig
      if (certPem.startsWith('data:')) {
        const base64Part = certPem.split(',')[1];
        if (base64Part) {
          certPem = Buffer.from(base64Part, 'base64').toString('utf-8');
        }
      }
      if (!certPem.includes('-----BEGIN CERTIFICATE-----')) {
        throw new BadRequestException('Ungueltiges Zertifikat-Format. PEM-Datei mit -----BEGIN CERTIFICATE----- erwartet.');
      }

      // Private Key validieren
      keyPem = body.privateKey?.trim();
      if (!keyPem) {
        throw new BadRequestException('Private Key ist erforderlich');
      }
      if (keyPem.startsWith('data:')) {
        const base64Part = keyPem.split(',')[1];
        if (base64Part) {
          keyPem = Buffer.from(base64Part, 'base64').toString('utf-8');
        }
      }
      if (!keyPem.includes('-----BEGIN') || !keyPem.includes('PRIVATE KEY-----')) {
        throw new BadRequestException('Ungueltiges Key-Format. PEM-Datei mit -----BEGIN PRIVATE KEY----- oder -----BEGIN RSA PRIVATE KEY----- erwartet.');
      }
    }

    // 5. Zertifikat parsen
    let certInfo: SslCertInfo;
    try {
      const x509 = new X509Certificate(certPem);
      const sanRaw = x509.subjectAltName || '';
      const sanList = sanRaw
        .split(',')
        .map((s: string) => s.trim().replace(/^DNS:/, ''))
        .filter((s: string) => s.length > 0);

      const now = new Date();
      const validFrom = new Date(x509.validFrom);
      const validTo = new Date(x509.validTo);
      const isExpired = now > validTo;

      // SAN-Match pruefen
      const dnsMatch = sanList.some((san: string) => {
        if (san.startsWith('*.')) {
          // Wildcard: *.firma.de matches sub.firma.de
          const domain = san.slice(2);
          return dnsName.endsWith(domain) && dnsName.split('.').length === san.split('.').length;
        }
        return san.toLowerCase() === dnsName.toLowerCase();
      });

      certInfo = {
        subject: x509.subject,
        issuer: x509.issuer,
        validFrom: validFrom.toISOString(),
        validTo: validTo.toISOString(),
        sanList,
        dnsMatch,
        isExpired,
        fingerprint: x509.fingerprint256,
      };

      if (isExpired) {
        throw new BadRequestException(
          `Zertifikat ist abgelaufen (gueltig bis ${validTo.toLocaleDateString('de-DE')})`,
        );
      }

      if (!dnsMatch) {
        warnings.push(
          `DNS-Name "${dnsName}" ist nicht im Zertifikat enthalten. SANs: ${sanList.join(', ') || 'keine'}`,
        );
      }
    } catch (err) {
      if (err instanceof BadRequestException) throw err;
      throw new BadRequestException(`Zertifikat konnte nicht gelesen werden: ${(err as Error).message}`);
    }

    // 6. Key-Validierung (kann als privater Schluessel geladen werden?)
    try {
      createPrivateKey(keyPem);
    } catch {
      throw new BadRequestException('Private Key konnte nicht gelesen werden. Stellen Sie sicher, dass der Key im PEM-Format vorliegt.');
    }

    // 7. Cert + Key Dateien schreiben
    try {
      await writeFile(join(this.certsPath, 'custom.crt'), certPem, 'utf-8');
      await writeFile(join(this.certsPath, 'custom.key'), keyPem, { encoding: 'utf-8', mode: 0o600 });
      this.logger.log('SSL-Zertifikat und Key geschrieben');
    } catch (err) {
      throw new BadRequestException(`Zertifikat-Dateien konnten nicht geschrieben werden: ${(err as Error).message}`);
    }

    // 8. Traefik Dynamic Config generieren (wenn enabled)
    if (body.enabled) {
      await this.writeTraefikSslConfig(dnsName);
      this.logger.log(`Traefik SSL-Config fuer ${dnsName} generiert`);
    } else {
      await this.removeTraefikSslConfig();
      this.logger.log('Traefik SSL-Config deaktiviert');
    }

    // 9. Metadata in Redis speichern
    const config: SslConfig = {
      enabled: body.enabled,
      dnsName,
      certificate: certInfo,
      lastUpdated: new Date().toISOString(),
    };
    await this.redis.set(SSL_CONFIG_KEY, JSON.stringify(config));

    this.logger.log(`SSL-Konfiguration gespeichert: ${dnsName}, enabled=${body.enabled}`);

    return { success: true, config, warnings };
  }

  /**
   * POST /api/v1/settings/ssl/check-dns
   * DNS-Aufloesung fuer einen Domainnamen pruefen.
   */
  @Post('ssl/check-dns')
  @Roles('PLATFORM_ADMIN')
  @UseGuards(RolesGuard)
  @ApiOperation({ summary: 'DNS-Aufloesung pruefen' })
  async checkDns(
    @Body() body: { dnsName: string },
  ): Promise<DnsCheckResponse> {
    const dnsName = body.dnsName?.trim();
    if (!dnsName) {
      throw new BadRequestException('dnsName ist erforderlich');
    }

    try {
      const resolver = new dns.Resolver();
      resolver.setServers(['8.8.8.8', '1.1.1.1']);

      const addresses = await new Promise<string[]>((resolve, reject) => {
        const timeout = setTimeout(() => reject(new Error('DNS-Timeout (5s)')), 5000);
        dns.resolve4(dnsName)
          .then((addrs) => { clearTimeout(timeout); resolve(addrs); })
          .catch((err) => { clearTimeout(timeout); reject(err); });
      });

      return { resolves: true, addresses };
    } catch (err) {
      return {
        resolves: false,
        addresses: [],
        error: (err as Error).message || 'DNS-Aufloesung fehlgeschlagen',
      };
    }
  }

  /**
   * DELETE /api/v1/settings/ssl
   * SSL deaktivieren und aufraumen.
   */
  @Delete('ssl')
  @Roles('PLATFORM_ADMIN')
  @UseGuards(RolesGuard)
  @ApiOperation({ summary: 'SSL/Domain-Konfiguration entfernen' })
  async deleteSslConfig(): Promise<{ success: boolean }> {
    // Traefik Dynamic Config entfernen
    await this.removeTraefikSslConfig();

    // Cert-Dateien entfernen
    await this.safeUnlink(join(this.certsPath, 'custom.crt'));
    await this.safeUnlink(join(this.certsPath, 'custom.key'));

    // Redis-Key entfernen
    await this.redis.del(SSL_CONFIG_KEY);

    this.logger.log('SSL-Konfiguration vollstaendig entfernt');

    return { success: true };
  }

  /**
   * Generiert die Traefik Dynamic Config fuer Custom-Domain-Routing.
   * Traefik erkennt Datei-Aenderungen automatisch (file.watch=true).
   */
  private async writeTraefikSslConfig(dnsName: string): Promise<void> {
    const escapedDns = dnsName.replace(/`/g, '');
    const yaml = `# Auto-generated by INSIGHT Platform — SSL/Domain Configuration
# DO NOT EDIT MANUALLY — Changes will be overwritten by the admin panel.
# Generated: ${new Date().toISOString()}

tls:
  certificates:
    - certFile: /certs/custom.crt
      keyFile: /certs/custom.key

http:
  routers:
    # HTTPS Frontend (Custom Domain)
    custom-frontend-secure:
      rule: "Host(\`${escapedDns}\`)"
      entrypoints:
        - websecure
      service: frontend@docker
      tls: {}
      priority: 10

    # HTTPS API (Custom Domain)
    custom-api-secure:
      rule: "Host(\`${escapedDns}\`) && PathPrefix(\`/api\`)"
      entrypoints:
        - websecure
      service: core-api@docker
      tls: {}
      priority: 110
      middlewares:
        - cors-api@file
        - security-headers@file

    # HTTPS CRM API (Custom Domain)
    custom-crm-secure:
      rule: "Host(\`${escapedDns}\`) && PathPrefix(\`/api/v1/crm\`)"
      entrypoints:
        - websecure
      service: crm@docker
      tls: {}
      priority: 120
      middlewares:
        - cors-api@file
        - security-headers@file

    # HTTP -> HTTPS Redirect (Custom Domain)
    custom-redirect:
      rule: "Host(\`${escapedDns}\`)"
      entrypoints:
        - web
      middlewares:
        - custom-https-redirect
      service: frontend@docker
      priority: 10

  middlewares:
    custom-https-redirect:
      redirectScheme:
        scheme: https
        permanent: true
`;

    await writeFile(join(this.dynamicConfigPath, 'ssl-domain.yml'), yaml, 'utf-8');
  }

  /**
   * Entfernt die Traefik SSL Dynamic Config.
   */
  private async removeTraefikSslConfig(): Promise<void> {
    await this.safeUnlink(join(this.dynamicConfigPath, 'ssl-domain.yml'));
  }

  /**
   * Loescht eine Datei ohne Fehler wenn sie nicht existiert.
   */
  private async safeUnlink(filePath: string): Promise<void> {
    try {
      await access(filePath);
      await unlink(filePath);
    } catch {
      // Datei existiert nicht — ok
    }
  }
}
