import { Provide, Inject, Scope, ScopeEnum, Logger } from '@midwayjs/core';
import { ILogger } from '@midwayjs/logger';
import { spawn } from '../../../comm/child_process.js';
import { pDataPath, pUploadPath } from '../../../comm/path.js';
import * as path from 'path';
import { SkillLoaderService } from './skill_loader.js';
import { SkillSecretService } from './skill_secret.js';
import { SkillConfigService } from './skill_config.js';
import { buildSkillRuntimeEnv, mergeSkillProcessEnv } from './skill_runtime_env.js';
import * as fs from 'fs';
import { sanitizeProcessPayload } from '../runtime/process_events.js';
import type {
  RuntimeProcessEventInput,
  RuntimeProcessLevel,
  RuntimeProcessStatus,
} from '../runtime/process_events.js';

const ENV_WHITELIST = [
  'PATH', 'HOME', 'USER', 'LANG', 'LC_ALL', 'TZ',
  'TEMP', 'TMP', 'TMPDIR',
  'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY',
  'SystemRoot', 'APPDATA', 'LOCALAPPDATA',
];

export interface SkillExecuteParams {
  skillName: string;
  input: Record<string, unknown>;
  agentId?: string;
  toolCallId?: string;
  onProcessEvent?: (event: RuntimeProcessEventInput) => void;
}

export interface SkillExecuteResult {
  success: boolean;
  output?: Record<string, unknown>;
  error?: string;
  duration: number;
}

export interface ResolveNodeRuntimeCommandOptions {
  isPkg?: boolean;
  platform?: NodeJS.Platform;
  configPath?: string;
  execPath?: string;
  existsSync?: (targetPath: string) => boolean;
}

export function getInstalledAppDir(options: {
  configPath?: string;
  execPath?: string;
} = {}): string {
  return path.dirname(options.configPath || process.env.NETA_CONFIG_PATH || options.execPath || process.execPath);
}

export function resolveNodeRuntimeCommand(options: ResolveNodeRuntimeCommandOptions = {}): string {
  const isPkg = options.isPkg ?? Boolean((process as any).pkg);
  const platform = options.platform ?? process.platform;
  const existsSync = options.existsSync ?? fs.existsSync;
  const appDir = getInstalledAppDir({
    configPath: options.configPath,
    execPath: options.execPath,
  });

  if (isPkg && platform === 'win32') {
    const bundledNode = path.join(appDir, 'node-runtime', 'node.exe');
    if (!existsSync(bundledNode)) {
      throw new Error(`Bundled Node runtime not found at ${bundledNode}. Reinstall Neta or rebuild the Windows installer with node-runtime included.`);
    }
    return bundledNode;
  }

  return 'node';
}

const PROCESS_EVENT_RESERVED_FIELDS = new Set([
  'type',
  'time',
  'timestamp',
  'stage',
  'message',
  'taskId',
  'level',
  'status',
  'current',
  'total',
  'percent',
  'batch',
  'totalBatches',
]);

const RUNTIME_PROCESS_STATUSES = new Set<RuntimeProcessStatus>([
  'queued',
  'started',
  'running',
  'completed',
  'failed',
  'cancelled',
  'skipped',
]);

const RUNTIME_PROCESS_LEVELS = new Set<RuntimeProcessLevel>([
  'debug',
  'info',
  'warning',
  'error',
]);

export function normalizeSkillProcessLine(
  skillName: string,
  line: string,
  sequence: number,
): RuntimeProcessEventInput | null {
  const jsonText = line.trim().replace(/^(\[[^\]]+\]\s*)+/, '').trim();
  if (!jsonText.startsWith('{')) return null;

  let parsed: Record<string, unknown>;
  try {
    const value = JSON.parse(jsonText);
    if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
    parsed = value as Record<string, unknown>;
  } catch {
    return null;
  }

  if (parsed.type !== 'process_event') return null;

  const status = typeof parsed.status === 'string' && RUNTIME_PROCESS_STATUSES.has(parsed.status as RuntimeProcessStatus)
    ? parsed.status as RuntimeProcessStatus
    : 'running';
  const level = typeof parsed.level === 'string' && RUNTIME_PROCESS_LEVELS.has(parsed.level as RuntimeProcessLevel)
    ? parsed.level as RuntimeProcessLevel
    : 'info';
  const stage = typeof parsed.stage === 'string' ? parsed.stage : undefined;
  const taskId = typeof parsed.taskId === 'string' ? parsed.taskId : undefined;
  const timestamp = typeof parsed.timestamp === 'string'
    ? parsed.timestamp
    : typeof parsed.time === 'string'
      ? parsed.time
      : undefined;
  const current = getFiniteNumber(parsed.current) ?? getFiniteNumber(parsed.batch);
  const total = getFiniteNumber(parsed.total) ?? getFiniteNumber(parsed.totalBatches);
  const explicitPercent = getFiniteNumber(parsed.percent);
  const percent = explicitPercent !== undefined
    ? clampSkillProcessPercent(explicitPercent)
    : computeSkillProcessPercent(current, total);
  const payload = Object.fromEntries(
    Object.entries(parsed).filter(([key]) => !PROCESS_EVENT_RESERVED_FIELDS.has(key)),
  );
  const sanitizedPayload = sanitizeProcessPayload(payload);

  return {
    source: 'skill',
    targetType: 'skill',
    taskId,
    stage,
    status,
    level,
    message: typeof parsed.message === 'string' ? parsed.message : stage || skillName,
    timestamp,
    sequence,
    current,
    total,
    percent,
    payload: sanitizedPayload && Object.keys(sanitizedPayload).length ? sanitizedPayload : undefined,
  };
}

function getFiniteNumber(value: unknown): number | undefined {
  return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
}

function computeSkillProcessPercent(current?: number, total?: number): number | undefined {
  if (current === undefined || total === undefined || total <= 0) return undefined;
  return clampSkillProcessPercent((current / total) * 100);
}

function clampSkillProcessPercent(value: number): number | undefined {
  if (!Number.isFinite(value)) return undefined;
  return Math.max(0, Math.min(100, Math.round(value)));
}

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

  @Inject()
  skillLoader: SkillLoaderService;

  @Inject()
  skillSecret: SkillSecretService;

  @Inject()
  skillConfig: SkillConfigService;

  async execute(params: SkillExecuteParams): Promise<SkillExecuteResult> {
    const startTime = Date.now();
    const { skillName, input } = params;

    const skill = this.skillLoader.getSkill(skillName);
    if (!skill) {
      return { success: false, error: `Skill "${skillName}" not found`, duration: 0 };
    }

    const config = this.skillConfig.getConfig(skillName);
    if (!config?.entrypoint) {
      return { success: false, error: `Skill "${skillName}" is not a compute-entry skill`, duration: 0 };
    }

    const skillDir = path.join(this.skillLoader.getSkillsDir(), skillName);
    const timeout = config.timeout || 30000;

    const resolvedEntry = path.resolve(skillDir, config.entrypoint);
    if (!resolvedEntry.startsWith(path.resolve(skillDir) + path.sep)) {
      return { success: false, error: 'Entrypoint path traversal detected', duration: 0 };
    }

    if ((config.runtime || 'python') === 'python') {
      const venvPath = path.join(skillDir, '.venv');
      try {
        const fs = await import('fs/promises');
        await fs.access(venvPath);
      } catch {
        return { success: false, error: `Python venv not found at ${venvPath}. Run dependency install first.`, duration: 0 };
      }
    }

    let command: { cmd: string; args: string[] };
    try {
      command = this.buildCommand(config.runtime || 'python', config.entrypoint, skillDir);
    } catch (e: any) {
      return { success: false, error: e?.message || String(e), duration: Date.now() - startTime };
    }
    const { cmd, args } = command;

    const baseEnv: Record<string, string> = {};
    for (const key of ENV_WHITELIST) {
      if (process.env[key]) baseEnv[key] = process.env[key]!;
    }
    const runtimeEnv = buildSkillRuntimeEnv({
      skillName,
      dataDir: pDataPath(),
      uploadRoot: pUploadPath(),
    });
    const skillEnv = await this.skillSecret.resolveEnv(skillName);
    const env = mergeSkillProcessEnv(baseEnv, skillEnv, runtimeEnv);

    return new Promise<SkillExecuteResult>((resolve) => {
      let stdout = '';
      let stderr = '';
      let stderrLineBuffer = '';
      let timedOut = false;
      let processEventSequence = 0;

      const emitProcessEvent = (event: RuntimeProcessEventInput) => {
        try {
          params.onProcessEvent?.(event);
        } catch {
          // Process events are observational only; callback failures must not fail skill execution.
        }
      };

      const emitFailureProcessEvent = (stage: string, message: string) => {
        emitProcessEvent({
          source: 'skill',
          targetType: 'skill',
          status: 'failed',
          level: 'error',
          stage,
          message,
          sequence: ++processEventSequence,
        });
      };

      const child = spawn(cmd, args, {
        cwd: skillDir,
        env,
        stdio: ['pipe', 'pipe', 'pipe'],
      });

      const timer = setTimeout(() => {
        timedOut = true;
        child.kill('SIGTERM');
      }, timeout);

      child.stdout.on('data', (chunk: Buffer) => { stdout += chunk.toString(); });
      const handleStderrLine = (line: string) => {
        const normalizedLine = line.trim();
        if (!normalizedLine) return;
        const processEvent = normalizeSkillProcessLine(skillName, normalizedLine, ++processEventSequence);
        if (processEvent) emitProcessEvent(processEvent);
        this.logger.info('[SkillExecutor][%s][stderr] %s', skillName, normalizedLine);
      };

      const flushStderrLines = (text: string) => {
        stderrLineBuffer += text;
        const lines = stderrLineBuffer.split(/\r?\n/);
        stderrLineBuffer = lines.pop() ?? '';
        for (const line of lines) {
          handleStderrLine(line);
        }
      };

      child.stderr.on('data', (chunk: Buffer) => {
        const text = chunk.toString();
        stderr += text;
        flushStderrLines(text);
      });

      child.stdin.write(JSON.stringify(input));
      child.stdin.end();

      child.on('close', (code) => {
        clearTimeout(timer);
        const duration = Date.now() - startTime;
        if (stderrLineBuffer.trim()) {
          handleStderrLine(stderrLineBuffer);
          stderrLineBuffer = '';
        }

        this.logger.info('[SkillExecutor] %s executed agent=%s duration=%dms exit=%d',
          skillName, params.agentId || 'unknown', duration, code);

        if (timedOut) {
          const stderrTail = stderr.trim().slice(-1000);
          const suffix = stderrTail ? ` Last stderr: ${stderrTail}` : '';
          const error = `Execution timed out after ${timeout}ms.${suffix}`;
          emitFailureProcessEvent('timeout', error);
          resolve({ success: false, error, duration });
          return;
        }

        if (code !== 0) {
          const errMsg = stderr.trim().slice(-1000) || `Process exited with code ${code}`;
          emitFailureProcessEvent('exit', errMsg);
          resolve({ success: false, error: errMsg, duration });
          return;
        }

        try {
          const output = JSON.parse(stdout.trim());
          resolve({ success: true, output, duration });
        } catch {
          const error = `Invalid JSON output: ${stdout.slice(0, 200)}`;
          emitFailureProcessEvent('output', error);
          resolve({ success: false, error, duration });
        }
      });

      child.on('error', (err) => {
        clearTimeout(timer);
        emitFailureProcessEvent('spawn', err.message);
        resolve({ success: false, error: err.message, duration: Date.now() - startTime });
      });
    });
  }

  private buildCommand(runtime: string, entrypoint: string, skillDir: string): { cmd: string; args: string[] } {
    const isWin = process.platform === 'win32';
    switch (runtime) {
      case 'python': {
        const python = isWin
          ? path.join(skillDir, '.venv', 'Scripts', 'python.exe')
          : path.join(skillDir, '.venv', 'bin', 'python');
        return { cmd: python, args: [entrypoint] };
      }
      case 'node':
        return { cmd: resolveNodeRuntimeCommand(), args: [entrypoint] };
      case 'bash':
        return { cmd: isWin ? 'bash' : '/bin/bash', args: [entrypoint] };
      case 'dotnet':
        return { cmd: 'dotnet', args: ['run', '--project', entrypoint] };
      default:
        return { cmd: runtime, args: [entrypoint] };
    }
  }
}
