import { Provide, Scope, ScopeEnum, Logger } from '@midwayjs/core';
import { ILogger } from '@midwayjs/logger';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import * as crypto from 'crypto';
import { NetaClawSkillEntity } from '../entity/skill.js';
import type { SkillEnvSchemaItem } from './skill_env_schema.js';

@Provide()
@Scope(ScopeEnum.Singleton)
export class SkillSecretService {
  @Logger()
  logger: ILogger;

  @InjectEntityModel(NetaClawSkillEntity)
  skillRepo: Repository<NetaClawSkillEntity>;

  private readonly algorithm = 'aes-256-gcm' as const;
  private readonly ivLength = 16;
  private readonly authTagLength = 16;

  private getLegacySecretKey(): string | undefined {
    const raw = process.env.SKILL_SECRET_KEY || process.env.APP_SECRET;
    return raw || undefined;
  }

  private deriveLegacyKey(raw: string): Buffer {
    return crypto.createHash('sha256').update(raw).digest();
  }

  encrypt(plainObj: Record<string, string>): string {
    return JSON.stringify(plainObj);
  }

  private encryptLegacy(plainObj: Record<string, string>, rawKey: string): string {
    const iv = crypto.randomBytes(this.ivLength);
    const key = this.deriveLegacyKey(rawKey);
    const cipher = crypto.createCipheriv(this.algorithm, key, iv);
    const encrypted = Buffer.concat([
      cipher.update(JSON.stringify(plainObj), 'utf8'),
      cipher.final(),
    ]);
    const authTag = cipher.getAuthTag();
    return Buffer.concat([iv, encrypted, authTag]).toString('base64');
  }

  decrypt(cipherText: string): Record<string, string> {
    try {
      const parsed = JSON.parse(cipherText);
      if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
        return Object.fromEntries(
          Object.entries(parsed).map(([key, value]) => [key, String(value)])
        );
      }
    } catch { /* fallback to legacy encrypted format */ }

    const rawKey = this.getLegacySecretKey();
    if (!rawKey) {
      throw new Error('Unable to read legacy encrypted skill secrets because SKILL_SECRET_KEY or APP_SECRET is not set');
    }
    return this.decryptLegacy(cipherText, rawKey);
  }

  private decryptLegacy(cipherText: string, rawKey: string): Record<string, string> {
    const buf = Buffer.from(cipherText, 'base64');
    const iv = buf.subarray(0, this.ivLength);
    const authTag = buf.subarray(buf.length - this.authTagLength);
    const encrypted = buf.subarray(this.ivLength, buf.length - this.authTagLength);
    const key = this.deriveLegacyKey(rawKey);
    const decipher = crypto.createDecipheriv(this.algorithm, key, iv);
    decipher.setAuthTag(authTag);
    const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
    return JSON.parse(decrypted.toString('utf8'));
  }

  async resolveEnv(skillName: string): Promise<Record<string, string>> {
    const entity = await this.skillRepo.findOneBy({ name: skillName });
    if (!entity) return {};

    const env: Record<string, string> = {};

    if (entity.envSchema) {
      for (const item of entity.envSchema) {
        if (Object.prototype.hasOwnProperty.call(item, 'default') && item.default !== undefined) {
          env[item.name] = item.default;
        }
      }
    }

    if (entity.secrets) {
      try {
        const secrets = this.decrypt(entity.secrets);
        Object.assign(env, secrets);
      } catch (e) {
        this.logger.error('[SkillSecret] 解密 %s secrets 失败: %s', skillName, e);
      }
    }

    return env;
  }

  async saveSecrets(skillName: string, secrets: Record<string, string>): Promise<void> {
    const encrypted = this.encrypt(secrets);
    await this.skillRepo.update({ name: skillName }, { secrets: encrypted });
  }

  async getConfiguredKeys(skillName: string): Promise<Array<{ name: string; hasValue: boolean }>> {
    const entity = await this.skillRepo.findOneBy({ name: skillName });
    if (!entity?.envSchema) return [];

    let configuredKeys = new Set<string>();
    if (entity.secrets) {
      try {
        const secrets = this.decrypt(entity.secrets);
        configuredKeys = new Set(Object.keys(secrets));
      } catch { /* ignore */ }
    }

    return entity.envSchema.map(item => ({
      name: item.name,
      hasValue: configuredKeys.has(item.name),
    }));
  }

  async getEnvSchema(skillName: string): Promise<SkillEnvSchemaItem[]> {
    const entity = await this.skillRepo.findOneBy({ name: skillName });
    return entity?.envSchema ?? [];
  }
}
