import { Injectable, Logger, InternalServerErrorException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { DataSource } from 'typeorm';
import { Client as SshClient, ConnectConfig } from 'ssh2';
import * as crypto from 'crypto';
import { AgentInstance } from '../../domain/entities/agent-instance.entity';
import { AgentInstanceRepository } from '../repositories/agent-instance.repository';

const OPENCLAW_IMAGE = 'hailin168/openclaw-bridge:latest';
const PORT_RANGE_START = 20000;
const PORT_RANGE_END = 29999;
const INTERNAL_API_KEY_HEADER = 'x-internal-api-key';

interface PoolServerCreds {
  id: string;
  host: string;
  sshPort: number;
  sshUser: string;
  sshKey: string; // decrypted private key
}

interface DeployOptions {
  instanceId: string;
  containerName: string;
  hostPort: number;
  userId: string;
  claudeApiKey: string;
  openclawToken: string;
  dingTalkClientId?: string;
  dingTalkClientSecret?: string;
}

@Injectable()
export class AgentInstanceDeployService {
  private readonly logger = new Logger(AgentInstanceDeployService.name);
  private readonly inventoryUrl: string;
  private readonly internalApiKey: string;
  private readonly encKey: string;
  private readonly agentServicePublicUrl: string;

  constructor(
    private readonly configService: ConfigService,
    private readonly instanceRepo: AgentInstanceRepository,
    private readonly dataSource: DataSource,
  ) {
    this.inventoryUrl = this.configService.get<string>('INVENTORY_SERVICE_URL', 'http://inventory-service:3004');
    this.internalApiKey = this.configService.get<string>('INTERNAL_API_KEY', '');
    this.encKey = this.configService.get<string>('VAULT_MASTER_KEY', 'dev-master-key');
    this.agentServicePublicUrl = this.configService.get<string>('AGENT_SERVICE_PUBLIC_URL', '');
  }

  // ── Public API ────────────────────────────────────────────────────────────

  async deployFromPool(
    instance: AgentInstance,
    claudeApiKey: string,
    dingTalkClientId?: string,
    dingTalkClientSecret?: string,
  ): Promise<void> {
    const server = await this.pickPoolServer();
    const creds = await this.fetchSshCreds(server.id);
    const hostPort = await this.allocatePort(creds.host);

    instance.serverHost = creds.host;
    instance.sshPort = creds.sshPort;
    instance.sshUser = creds.sshUser;
    instance.hostPort = hostPort;
    instance.poolServerId = server.id;

    await this.runDeploy(instance, creds, claudeApiKey, dingTalkClientId, dingTalkClientSecret);
    await this.notifyPoolIncrement(server.id);
  }

  async deployToUserServer(
    instance: AgentInstance,
    sshKey: string,
    claudeApiKey: string,
    dingTalkClientId?: string,
    dingTalkClientSecret?: string,
  ): Promise<void> {
    const creds: PoolServerCreds = {
      id: '',
      host: instance.serverHost!,
      sshPort: instance.sshPort,
      sshUser: instance.sshUser!,
      sshKey,
    };
    const hostPort = await this.allocatePort(creds.host);
    instance.hostPort = hostPort;

    await this.runDeploy(instance, creds, claudeApiKey, dingTalkClientId, dingTalkClientSecret);
  }

  async stopInstance(instance: AgentInstance): Promise<void> {
    const sshKey = instance.poolServerId
      ? (await this.fetchSshCreds(instance.poolServerId)).sshKey
      : undefined;

    if (!sshKey) {
      throw new InternalServerErrorException('Cannot stop user-server instance without SSH key');
    }

    await this.sshExec(
      { host: instance.serverHost, port: instance.sshPort, username: instance.sshUser, privateKey: sshKey },
      `docker stop ${instance.containerName} || true`,
    );
  }

  async removeInstance(instance: AgentInstance): Promise<void> {
    if (instance.poolServerId) {
      const creds = await this.fetchSshCreds(instance.poolServerId);
      await this.sshExec(
        { host: instance.serverHost, port: instance.sshPort, username: instance.sshUser, privateKey: creds.sshKey },
        `docker rm -f ${instance.containerName} || true`,
      );
      await this.notifyPoolDecrement(instance.poolServerId);
    }
  }

  // ── Encryption helpers ────────────────────────────────────────────────────

  encryptToken(plaintext: string): { encrypted: string; iv: string } {
    const ivBuf = crypto.randomBytes(16);
    const keyBuf = crypto.scryptSync(this.encKey, 'salt', 32);
    const cipher = crypto.createCipheriv('aes-256-cbc', keyBuf, ivBuf);
    const enc = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
    return { encrypted: enc.toString('base64'), iv: ivBuf.toString('base64') };
  }

  decryptToken(encrypted: string, iv: string): string {
    const keyBuf = crypto.scryptSync(this.encKey, 'salt', 32);
    const decipher = crypto.createDecipheriv(
      'aes-256-cbc',
      keyBuf,
      Buffer.from(iv, 'base64'),
    );
    return Buffer.concat([
      decipher.update(Buffer.from(encrypted, 'base64')),
      decipher.final(),
    ]).toString('utf8');
  }

  // ── Private helpers ───────────────────────────────────────────────────────

  private async pickPoolServer(): Promise<{ id: string }> {
    const res = await fetch(
      `${this.inventoryUrl}/api/v1/inventory/pool-servers?available=true`,
      { headers: { [INTERNAL_API_KEY_HEADER]: this.internalApiKey } },
    );
    if (!res.ok) throw new InternalServerErrorException('Failed to query pool servers');
    const servers: any[] = await res.json();
    if (!servers.length) throw new InternalServerErrorException('No available pool servers');
    return servers[0]; // Already sorted by current_instances ASC
  }

  private async fetchSshCreds(poolServerId: string): Promise<PoolServerCreds> {
    const res = await fetch(
      `${this.inventoryUrl}/api/v1/inventory/pool-servers/${poolServerId}/deploy-creds`,
      { headers: { [INTERNAL_API_KEY_HEADER]: this.internalApiKey } },
    );
    if (!res.ok) throw new InternalServerErrorException('Failed to fetch SSH credentials');
    return res.json() as Promise<PoolServerCreds>;
  }

  private async allocatePort(serverHost: string): Promise<number> {
    const used = await this.instanceRepo.getUsedPortsOnServer(this.dataSource, serverHost);
    for (let p = PORT_RANGE_START; p <= PORT_RANGE_END; p++) {
      if (!used.includes(p)) return p;
    }
    throw new InternalServerErrorException('No available ports on server');
  }

  private async runDeploy(
    instance: AgentInstance,
    creds: PoolServerCreds,
    claudeApiKey: string,
    dingTalkClientId?: string,
    dingTalkClientSecret?: string,
  ): Promise<void> {
    const token = crypto.randomBytes(32).toString('hex');
    const { encrypted, iv } = this.encryptToken(token);
    instance.openclawToken = encrypted;
    instance.openclawTokenIv = iv;

    const envParts = [
      `-e OPENCLAW_GATEWAY_TOKEN=${token}`,
      `-e IT0_INSTANCE_ID=${instance.id}`,
      `-e CLAUDE_API_KEY=${claudeApiKey}`,
      `-e IT0_AGENT_SERVICE_URL=${this.agentServicePublicUrl}`,
    ];
    if (dingTalkClientId && dingTalkClientSecret) {
      envParts.push(`-e DINGTALK_CLIENT_ID=${dingTalkClientId}`);
      envParts.push(`-e DINGTALK_CLIENT_SECRET=${dingTalkClientSecret}`);
    }

    const cmd = [
      'docker run -d',
      `--name ${instance.containerName}`,
      `--restart unless-stopped`,
      `-p ${instance.hostPort}:3000`,
      `-v /data/openclaw/${instance.id}:/home/node/.openclaw`,
      ...envParts,
      OPENCLAW_IMAGE,
    ].join(' ');

    this.logger.log(`Deploying ${instance.containerName} on ${creds.host}:${instance.hostPort}`);

    await this.sshExec(
      { host: creds.host, port: creds.sshPort, username: creds.sshUser, privateKey: creds.sshKey },
      cmd,
    );

    instance.status = 'running';
  }

  private async notifyPoolIncrement(poolServerId: string): Promise<void> {
    await fetch(
      `${this.inventoryUrl}/api/v1/inventory/pool-servers/${poolServerId}/increment`,
      { method: 'POST', headers: { [INTERNAL_API_KEY_HEADER]: this.internalApiKey } },
    );
  }

  private async notifyPoolDecrement(poolServerId: string): Promise<void> {
    await fetch(
      `${this.inventoryUrl}/api/v1/inventory/pool-servers/${poolServerId}/decrement`,
      { method: 'POST', headers: { [INTERNAL_API_KEY_HEADER]: this.internalApiKey } },
    );
  }

  private sshExec(config: ConnectConfig, command: string): Promise<string> {
    return new Promise((resolve, reject) => {
      const conn = new SshClient();
      let output = '';

      conn
        .on('ready', () => {
          conn.exec(command, (err, stream) => {
            if (err) { conn.end(); reject(err); return; }

            stream
              .on('close', (code: number) => {
                conn.end();
                if (code !== 0) {
                  reject(new Error(`SSH command exited with code ${code}: ${output}`));
                } else {
                  resolve(output);
                }
              })
              .on('data', (data: Buffer) => { output += data.toString(); })
              .stderr.on('data', (data: Buffer) => { output += data.toString(); });
          });
        })
        .on('error', reject)
        .connect(config);
    });
  }
}
