import {
  Injectable,
  Logger,
  BadRequestException,
  InternalServerErrorException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
import { lookup } from 'node:dns';
import { isIP } from 'node:net';
import * as http from 'node:http';
import * as https from 'node:https';

import { AiConfig, AiProvider, SINGLETON_KEY } from './ai-config.entity';
import {
  SaveAiConfigDto,
  TestAiConfigDto,
  AiConfigResponseDto,
  AiConfigTestResultDto,
  AiRuntimeConfig,
  DEFAULT_BASE_URLS,
} from './dto/ai-config.dto';

// ---------------------------------------------------------------------------
// Key derivation
// ---------------------------------------------------------------------------

let _encryptionWarned = false;

function getEncryptionKey(): Buffer {
  const raw = process.env.AI_CONFIG_ENCRYPTION_KEY;
  if (!raw) {
    if (process.env.NODE_ENV !== 'production') {
      if (!_encryptionWarned) {
        _encryptionWarned = true;
        Logger.warn(
          'AI_CONFIG_ENCRYPTION_KEY 未设置，使用开发回退密钥。生产环境必须配置！',
          'AiConfigService',
        );
      }
      // 32 hex pairs → 32 bytes
      return Buffer.from('ff'.repeat(32), 'hex');
    }
    throw new InternalServerErrorException('AI_CONFIG_ENCRYPTION_KEY 未配置，无法加解密 API Key');
  }

  // Hex: exactly 64 hex chars
  if (/^[0-9a-fA-F]{64}$/.test(raw)) {
    return Buffer.from(raw, 'hex');
  }

  // Base64: decode then re-encode to normalize padding; reject non-canonical forms
  if (/^[A-Za-z0-9+/]+=*$/.test(raw)) {
    const buf = Buffer.from(raw, 'base64');
    if (buf.length !== 32) {
      throw new InternalServerErrorException(
        'AI_CONFIG_ENCRYPTION_KEY 格式无效：base64 解码后须为 32 字节',
      );
    }
    // Re-encode to canonical base64 (no line breaks) and compare
    const canonical = buf.toString('base64');
    if (raw !== canonical) {
      throw new InternalServerErrorException(
        'AI_CONFIG_ENCRYPTION_KEY 格式无效：base64 编码须为标准格式（无多余 padding）',
      );
    }
    return buf;
  }

  throw new InternalServerErrorException(
    'AI_CONFIG_ENCRYPTION_KEY 格式无效：需为 64 位 hex 或 base64 编码的 32 字节密钥',
  );
}

const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 12;
const AUTH_TAG_LENGTH = 16;

function encrypt(plaintext: string): { ciphertext: string; iv: string; authTag: string } {
  const key = getEncryptionKey();
  const iv = randomBytes(IV_LENGTH);
  const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
  const encrypted = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);
  const tag = cipher.getAuthTag();
  return {
    ciphertext: encrypted.toString('base64'),
    iv: iv.toString('base64'),
    authTag: tag.toString('base64'),
  };
}

function decrypt(ciphertextB64: string, ivB64: string, authTagB64: string): string {
  const key = getEncryptionKey();
  const iv = Buffer.from(ivB64, 'base64');
  const authTag = Buffer.from(authTagB64, 'base64');
  const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
  decipher.setAuthTag(authTag);
  const decrypted = Buffer.concat([
    decipher.update(Buffer.from(ciphertextB64, 'base64')),
    decipher.final(),
  ]);
  return decrypted.toString('utf-8');
}

// ---------------------------------------------------------------------------
// URL / SSRF helpers
// ---------------------------------------------------------------------------

const PRIVATE_IPV4_RANGES = [
  /^127\./,
  /^10\./,
  /^172\.(1[6-9]|2\d|3[01])\./,
  /^192\.168\./,
  /^169\.254\./,
  /^0\./,
  /^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./,
];
function isPrivateHost(hostname: string): boolean {
  // Strip IPv6 brackets from URL.hostname
  if (hostname.startsWith('[') && hostname.endsWith(']')) {
    hostname = hostname.slice(1, -1);
  }

  if (hostname === 'localhost' || hostname === '0.0.0.0') return true;
  if (hostname.endsWith('.local')) return true;

  if (isIP(hostname) === 6) {
    // IPv6 private/loopback
    if (hostname === '::1' || hostname === '::') return true;
    const lower = hostname.toLowerCase();
    if (lower.startsWith('fc') || lower.startsWith('fd')) return true; // fc00::/7
    if (
      lower.startsWith('fe8') ||
      lower.startsWith('fe9') ||
      lower.startsWith('fea') ||
      lower.startsWith('feb')
    )
      return true; // fe80::/10
    // IPv4-mapped IPv6: ::ffff:0:0/96
    if (lower.startsWith('::ffff:') && isIP(lower.slice(7)) === 4) {
      return PRIVATE_IPV4_RANGES.some((re) => re.test(lower.slice(7)));
    }
    return false;
  }

  if (isIP(hostname) === 4) {
    return PRIVATE_IPV4_RANGES.some((re) => re.test(hostname));
  }

  return false;
}

// Known provider hosts — only these are allowed for fixed providers
const PROVIDER_HOSTS: Partial<Record<AiProvider, readonly string[]>> = {
  [AiProvider.OPENAI]: ['api.openai.com'],
  [AiProvider.DEEPSEEK]: ['api.deepseek.com'],
};

// Required pathname for fixed providers
const PROVIDER_REQUIRED_PATHS: Partial<Record<AiProvider, string>> = {
  [AiProvider.OPENAI]: '/v1',
  [AiProvider.DEEPSEEK]: '/',
};

function validateAndNormalizeBaseUrl(url: string | undefined, provider: AiProvider): string {
  const allowPrivate = process.env.AI_ALLOW_PRIVATE_BASE_URL === 'true';

  const raw = url?.trim() || DEFAULT_BASE_URLS[provider];
  if (!raw) {
    throw new BadRequestException('OPENAI_COMPATIBLE 模式必须提供 baseUrl');
  }

  // Reject search/query and hash/fragment
  let parsed: URL;
  try {
    parsed = new URL(raw);
  } catch {
    throw new BadRequestException('请求参数无效');
  }

  if (parsed.search || parsed.hash) {
    throw new BadRequestException('请求参数无效');
  }

  if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
    throw new BadRequestException('请求参数无效');
  }

  if (process.env.NODE_ENV === 'production' && parsed.protocol === 'http:') {
    throw new BadRequestException('生产环境禁止使用 http://');
  }

  if (parsed.username || parsed.password) {
    throw new BadRequestException('请求参数无效');
  }

  const normalized = parsed.origin + parsed.pathname.replace(/\/+$/, '');

  // Provider-specific host check
  const allowedHosts = PROVIDER_HOSTS[provider];
  if (allowedHosts) {
    if (!allowedHosts.includes(parsed.hostname)) {
      throw new BadRequestException(`${provider} 必须使用固定域名`);
    }
    // Enforce exact path for fixed providers
    const requiredPath = PROVIDER_REQUIRED_PATHS[provider];
    if (
      requiredPath !== undefined &&
      parsed.pathname.replace(/\/+$/, '') !== requiredPath.replace(/\/+$/, '')
    ) {
      throw new BadRequestException(`请求参数无效`);
    }
  } else {
    // OPENAI_COMPATIBLE — SSRF check
    if (!allowPrivate && isPrivateHost(parsed.hostname)) {
      throw new BadRequestException('不允许使用内网地址');
    }
  }

  return normalized;
}

async function resolveHostnames(hostname: string): Promise<{ address: string; family: number }[]> {
  return new Promise((resolve, reject) => {
    lookup(hostname, { all: true, family: 0 }, (err, addresses) => {
      if (err) {
        reject(err);
        return;
      }
      if (!addresses || addresses.length === 0) {
        reject(new Error('DNS 解析返回空结果'));
        return;
      }
      resolve(
        addresses.map((a) => ({
          address: a.address,
          family: a.family,
        })),
      );
    });
  });
}

async function validateDnsNotPrivate(hostname: string): Promise<void> {
  const allowPrivate = process.env.AI_ALLOW_PRIVATE_BASE_URL === 'true';
  if (allowPrivate) return;

  let addresses: { address: string; family: number }[];
  try {
    addresses = await resolveHostnames(hostname);
  } catch {
    throw new BadRequestException('无法解析域名');
  }

  for (const { address } of addresses) {
    if (isPrivateHost(address)) {
      throw new BadRequestException('域名解析到内网地址');
    }
  }
}

// ---------------------------------------------------------------------------
// Connection test — uses node:http/https with DNS pinning to prevent rebinding
// ---------------------------------------------------------------------------

const MAX_RESPONSE_BYTES = 1_048_576; // 1 MiB

/**
 * Perform a pinned HTTP GET request.
 * DNS resolves once; the resolved IP is used for connection, preventing DNS rebinding.
 * Redirects are forbidden. HTTPS certificate validation is enforced.
 */
function pinnedGet(
  url: string,
  headers: Record<string, string>,
  timeoutMs: number,
): Promise<{ status: number; contentType: string | null; body: string; latencyMs: number }> {
  return new Promise((resolve, reject) => {
    const parsed = new URL(url);
    const isHttps = parsed.protocol === 'https:';
    const port = parsed.port ? parseInt(parsed.port, 10) : isHttps ? 443 : 80;
    const hostname = parsed.hostname;
    const path = parsed.pathname + parsed.search;

    lookup(hostname, { all: true, family: 0 }, (dnsErr, addresses) => {
      if (dnsErr || !addresses || addresses.length === 0) {
        reject(new Error('DNS 解析失败'));
        return;
      }

      const resolved = addresses.find((a) => !isPrivateHost(a.address));
      if (!resolved && process.env.AI_ALLOW_PRIVATE_BASE_URL !== 'true') {
        reject(new Error('解析到内网地址'));
        return;
      }
      const targetIp = resolved ? resolved.address : addresses[0].address;
      const family = resolved ? resolved.family : addresses[0].family;

      const transport = isHttps ? https : http;

      const requestStart = Date.now();

      const req = transport.request(
        {
          hostname: targetIp,
          port,
          path,
          method: 'GET',
          headers: { ...headers, Host: hostname },
          servername: isHttps ? hostname : undefined,
          rejectUnauthorized: isHttps,
          family: family === 6 ? 6 : 4,
          timeout: timeoutMs,
        },
        (res) => {
          const latencyMs = Date.now() - requestStart;
          const status = res.statusCode ?? 500;
          if (status >= 300 && status < 400 && res.headers.location) {
            res.resume();
            res.destroy();
            return reject(new Error('禁止重定向'));
          }

          const contentType = res.headers['content-type'] ?? null;

          const chunks: Buffer[] = [];
          let totalBytes = 0;

          res.on('data', (chunk: Buffer) => {
            totalBytes += chunk.length;
            if (totalBytes > MAX_RESPONSE_BYTES) {
              res.destroy();
              reject(new Error('响应过大'));
              return;
            }
            chunks.push(chunk);
          });
          res.on('end', () => {
            const body = Buffer.concat(chunks).toString('utf-8');
            resolve({ status, contentType, body, latencyMs });
          });

          res.on('error', reject);
        },
      );

      req.on('timeout', () => {
        req.destroy();
        reject(new Error('连接超时'));
      });

      req.on('error', reject);
      req.end();
    });
  });
}

// ---------------------------------------------------------------------------
// Service
// ---------------------------------------------------------------------------

@Injectable()
export class AiConfigService {
  private readonly logger = new Logger(AiConfigService.name);

  constructor(
    @InjectRepository(AiConfig)
    private readonly repo: Repository<AiConfig>,
  ) {}

  /** Resolve the effective API key: DB first, then env, then none */
  private resolveApiKey(config: AiConfig | null): {
    plaintext: string | null;
    source: 'database' | 'environment' | 'none';
  } {
    // DB stored key
    if (config?.encryptedApiKey && config?.apiKeyIv && config?.apiKeyAuthTag) {
      try {
        const plaintext = decrypt(config.encryptedApiKey, config.apiKeyIv, config.apiKeyAuthTag);
        return { plaintext, source: 'database' };
      } catch {
        this.logger.error('解密数据库 API Key 失败，密文可能已损坏');
        throw new InternalServerErrorException('无法解密 API Key');
      }
    }

    // Environment fallback
    const envKey = process.env.AI_API_KEY;
    if (envKey) {
      return { plaintext: envKey, source: 'environment' };
    }

    return { plaintext: null, source: 'none' };
  }

  /** Load or create the singleton config row */
  async getOrCreateConfig(): Promise<AiConfig> {
    let config = await this.repo.findOne({ where: { singletonKey: SINGLETON_KEY } });
    if (!config) {
      config = this.repo.create({
        singletonKey: SINGLETON_KEY,
        provider: AiProvider.OPENAI,
        baseUrl: DEFAULT_BASE_URLS[AiProvider.OPENAI],
        enabled: false,
        timeoutMs: 30000,
      });
      try {
        config = await this.repo.save(config);
      } catch (err: unknown) {
        // Unique constraint violation → another request created it first
        const isErrWithCode = err !== null && typeof err === 'object' && 'code' in err;
        const code = isErrWithCode ? (err as Record<string, unknown>).code : undefined;
        const errno = isErrWithCode ? (err as Record<string, unknown>).errno : undefined;
        // MySQL: ER_DUP_ENTRY (code 'ER_DUP_ENTRY') or errno 1062
        // SQLite: SQLITE_CONSTRAINT (code 'SQLITE_CONSTRAINT')
        if (code === 'ER_DUP_ENTRY' || errno === 1062 || code === 'SQLITE_CONSTRAINT') {
          const existing = await this.repo.findOne({ where: { singletonKey: SINGLETON_KEY } });
          if (existing) return existing;
        }
        throw err;
      }
    }
    return config;
  }

  /** Build masked key display */
  private buildMaskedKey(keyLast4: string | null): string | null {
    if (keyLast4 && keyLast4.length === 4) {
      return `••••${keyLast4}`;
    }
    return null;
  }

  /** GET response */
  async getConfig(): Promise<AiConfigResponseDto> {
    const config = await this.getOrCreateConfig();
    const { source } = this.resolveApiKey(config);

    const hasDbKey = !!(config.encryptedApiKey && config.apiKeyIv && config.apiKeyAuthTag);

    return {
      id: config.id,
      provider: config.provider,
      baseUrl: config.baseUrl,
      hasApiKey: source !== 'none',
      hasDatabaseKey: hasDbKey,
      maskedApiKey: config.keyLast4
        ? this.buildMaskedKey(config.keyLast4)
        : source !== 'none'
          ? '••••'
          : null,
      keySource: source,
      defaultModel: config.defaultModel ?? null,
      enabled: config.enabled,
      timeoutMs: config.timeoutMs,
      verified: config.verified,
      lastTestedAt: config.lastTestedAt?.toISOString() ?? null,
      lastTestLatencyMs: config.lastTestLatencyMs ?? null,
      createdAt: config.createdAt.toISOString(),
      updatedAt: config.updatedAt.toISOString(),
    };
  }

  /** PUT / save */
  async saveConfig(dto: SaveAiConfigDto): Promise<AiConfig> {
    const config = await this.getOrCreateConfig();

    // Validate and normalize baseUrl
    const normalizedBaseUrl = validateAndNormalizeBaseUrl(dto.baseUrl, dto.provider);

    // DNS SSRF check for all providers
    await validateDnsNotPrivate(new URL(normalizedBaseUrl).hostname);

    config.provider = dto.provider;
    config.baseUrl = normalizedBaseUrl;

    if (dto.defaultModel !== undefined) {
      config.defaultModel = dto.defaultModel || null;
    }

    if (dto.timeoutMs !== undefined) {
      config.timeoutMs = dto.timeoutMs;
    }

    // Handle apiKey — empty/undefined = keep existing
    if (dto.apiKey !== undefined && dto.apiKey !== '') {
      const { ciphertext, iv, authTag } = encrypt(dto.apiKey);
      config.encryptedApiKey = ciphertext;
      config.apiKeyIv = iv;
      config.apiKeyAuthTag = authTag;
      config.keyLast4 = dto.apiKey.slice(-4);
    }

    // enabled validation
    if (dto.enabled !== undefined) {
      if (dto.enabled) {
        const { plaintext } = this.resolveApiKey(config);
        if (!plaintext) {
          throw new BadRequestException('未配置 API Key，无法启用。请先保存 API Key 再启用');
        }
        // defaultModel is required when enabled
        const effectiveDefaultModel =
          dto.defaultModel !== undefined ? dto.defaultModel : config.defaultModel;
        if (!effectiveDefaultModel) {
          throw new BadRequestException('启用 AI 服务时必须配置默认模型');
        }
      }
      config.enabled = dto.enabled;
    }

    return this.repo.save(config);
  }

  /** Clear DB key only */
  async clearKey(): Promise<AiConfigResponseDto> {
    const config = await this.getOrCreateConfig();
    config.encryptedApiKey = null;
    config.apiKeyIv = null;
    config.apiKeyAuthTag = null;
    config.keyLast4 = null;
    // If no env key either, disable
    const envKey = process.env.AI_API_KEY;
    if (!envKey) {
      config.enabled = false;
    }
    await this.repo.save(config);

    return this.getConfig();
  }

  /** Test connection — uses saved config or request body overrides */
  async testConnection(dto?: TestAiConfigDto): Promise<AiConfigTestResultDto> {
    const config = await this.getOrCreateConfig();
    const now = new Date().toISOString();

    // Determine effective provider / baseUrl
    const provider = dto?.provider ?? config.provider;
    const rawBaseUrl = dto?.baseUrl ?? config.baseUrl;
    let baseUrl: string;
    try {
      baseUrl = validateAndNormalizeBaseUrl(rawBaseUrl, provider);
    } catch (err: unknown) {
      const message = err instanceof BadRequestException ? err.message : '请求参数无效';
      return {
        success: false,
        latencyMs: null,
        modelCount: null,
        modelAvailable: false,
        testedAt: now,
        message,
      };
    }

    // Determine effective defaultModel
    const effectiveDefaultModel = dto?.defaultModel ?? config.defaultModel ?? '';

    // DNS check
    try {
      await validateDnsNotPrivate(new URL(baseUrl).hostname);
    } catch (err: unknown) {
      const message = err instanceof BadRequestException ? err.message : '请求参数无效';
      return {
        success: false,
        latencyMs: null,
        modelCount: null,
        modelAvailable: false,
        testedAt: now,
        message,
      };
    }

    // Determine API key
    let apiKey: string;
    if (dto?.apiKey) {
      apiKey = dto.apiKey;
    } else {
      const { plaintext } = this.resolveApiKey(config);
      if (!plaintext) {
        return {
          success: false,
          latencyMs: null,
          modelCount: null,
          modelAvailable: false,
          testedAt: now,
          message: '未配置 API Key',
        };
      }
      apiKey = plaintext;
    }

    const timeoutMs = dto?.timeoutMs ?? config.timeoutMs;

    let result: AiConfigTestResultDto;
    try {
      const { status, contentType, body, latencyMs } = await pinnedGet(
        `${baseUrl}/models`,
        { Authorization: `Bearer ${apiKey}` },
        timeoutMs,
      );

      // Classify by HTTP status first, then content-type
      if (status === 401 || status === 403) {
        result = {
          success: false,
          latencyMs,
          modelCount: null,
          modelAvailable: false,
          testedAt: now,
          message: '认证失败，请检查 API Key',
        };
      } else if (status >= 500) {
        result = {
          success: false,
          latencyMs,
          modelCount: null,
          modelAvailable: false,
          testedAt: now,
          message: '服务不可用',
        };
      } else if (status >= 400) {
        result = {
          success: false,
          latencyMs,
          modelCount: null,
          modelAvailable: false,
          testedAt: now,
          message: `服务返回错误状态 ${status}`,
        };
      } else if (!contentType || !contentType.includes('application/json')) {
        result = {
          success: false,
          latencyMs,
          modelCount: null,
          modelAvailable: false,
          testedAt: now,
          message: '响应格式无效',
        };
      } else {
        let data: { data?: Array<{ id: string }> };
        try {
          const parsed: unknown = JSON.parse(body);
          if (!parsed || typeof parsed !== 'object') throw new Error('invalid');
          data = parsed;
        } catch {
          result = {
            success: false,
            latencyMs,
            modelCount: null,
            modelAvailable: false,
            testedAt: now,
            message: '响应格式无效',
          };
          config.lastTestedAt = new Date();
          config.lastTestLatencyMs = latencyMs;
          config.verified = false;
          await this.repo.save(config);
          return result;
        }

        const models = Array.isArray(data?.data) ? data.data : [];
        const modelCount = models.length;
        const modelAvailable =
          !effectiveDefaultModel || models.some((m) => m.id === effectiveDefaultModel);

        const message = modelAvailable
          ? `连接成功，目标模型 "${effectiveDefaultModel}" 可用`
          : effectiveDefaultModel
            ? '连接成功，但未找到目标模型'
            : models.length > 0
              ? `连接成功，可用模型 ${models.length} 个`
              : '连接成功，但未返回可用模型';

        result = {
          success: true,
          latencyMs,
          modelCount,
          modelAvailable,
          testedAt: now,
          message,
        };
      }
    } catch (err: unknown) {
      const message =
        err instanceof Error
          ? err.message === '连接超时'
            ? '连接超时'
            : err.message === '响应过大'
              ? '响应过大'
              : err.message === '禁止重定向'
                ? '连接失败，请检查 Base URL'
                : '连接失败，请检查 Base URL'
          : '连接失败，请检查 Base URL';
      result = {
        success: false,
        latencyMs: null,
        modelCount: null,
        modelAvailable: false,
        testedAt: now,
        message,
      };
    }

    // Update last tested info on config
    config.lastTestedAt = new Date();
    config.lastTestLatencyMs = result.latencyMs;
    config.verified = result.success;
    await this.repo.save(config);
    return result;
  }

  /**
   * Server-only runtime config — for future AI adapters.
   * Re-validates the stored base URL and DNS at runtime to guard
   * against config-table tampering or DNS record changes.
   * Future adapters should still use a restricted transport helper.
   */
  async getRuntimeConfig(): Promise<AiRuntimeConfig> {
    const config = await this.repo.findOne({ where: { singletonKey: SINGLETON_KEY } });
    if (!config) {
      throw new InternalServerErrorException('AI 配置未初始化');
    }

    if (!config.enabled) {
      throw new BadRequestException('AI 服务未启用');
    }

    // Re-validate and normalize the stored base URL
    const normalizedBaseUrl = validateAndNormalizeBaseUrl(config.baseUrl, config.provider);

    // Re-check DNS at runtime
    await validateDnsNotPrivate(new URL(normalizedBaseUrl).hostname);

    const { plaintext } = this.resolveApiKey(config);
    if (!plaintext) {
      throw new BadRequestException('未配置 API Key');
    }

    // defaultModel is required for actual AI calls
    if (!config.defaultModel) {
      throw new BadRequestException('未配置默认模型');
    }

    return {
      provider: config.provider,
      baseUrl: normalizedBaseUrl,
      apiKey: plaintext,
      defaultModel: config.defaultModel,
      timeoutMs: config.timeoutMs,
      enabled: config.enabled,
    };
  }
}
