import fs from 'fs';
import path from 'path';
import { execFileSync } from 'child_process';

export interface TtsRequest {
  text: string;
  voice?: string;
  provider?: string;
}

export interface TtsResult {
  provider: string;
  mode: 'local' | 'remote';
  filePath: string;
  mediaType: string;
}

export interface TtsProvider {
  id: string;
  mode: 'local' | 'remote';
  synthesize(request: TtsRequest, env?: NodeJS.ProcessEnv): TtsResult;
}

export const DEFAULT_TTS_PROVIDER = 'edge';
export const DEFAULT_EDGE_TTS_VOICE = 'en-GB-SoniaNeural';
export const MAX_TTS_TEXT_LENGTH = 2000;
export const TTS_OUTBOUND_DIR = '/workspace/group/.nanoclaw-outbound/tts';

export function getTtsOutboundDir(env: NodeJS.ProcessEnv = process.env): string {
  return env.NANOCLAW_TTS_OUTBOUND_DIR || TTS_OUTBOUND_DIR;
}

export function ensureTtsOutboundDir(env: NodeJS.ProcessEnv = process.env): string {
  const dir = getTtsOutboundDir(env);
  fs.mkdirSync(dir, { recursive: true });
  return dir;
}

function createTtsOutputPath(extension = '.mp3', env: NodeJS.ProcessEnv = process.env): string {
  const outboundDir = ensureTtsOutboundDir(env);
  const tempDir = fs.mkdtempSync(path.join(outboundDir, 'speech-'));
  return path.join(tempDir, `voice${extension}`);
}

export class EdgeTtsProvider implements TtsProvider {
  id = 'edge';
  mode: 'remote' = 'remote';

  synthesize(request: TtsRequest, env: NodeJS.ProcessEnv = process.env): TtsResult {
    const edgeTtsBin = env.NANOCLAW_EDGE_TTS_BIN || 'edge-tts';
    const voice = request.voice || env.NANOCLAW_TTS_DEFAULT_VOICE || DEFAULT_EDGE_TTS_VOICE;
    const outputPath = createTtsOutputPath('.mp3', env);

    execFileSync(
      edgeTtsBin,
      [
        '--text',
        request.text,
        '--voice',
        voice,
        '--write-media',
        outputPath,
      ],
      { stdio: 'pipe' },
    );

    if (!fs.existsSync(outputPath) || fs.statSync(outputPath).size === 0) {
      throw new Error(`TTS provider ${this.id} produced no audio output`);
    }

    return {
      provider: this.id,
      mode: this.mode,
      filePath: outputPath,
      mediaType: 'audio/mpeg',
    };
  }
}

export class QwenTtsProvider implements TtsProvider {
  id = 'qwen';
  mode: 'local' = 'local';

  synthesize(request: TtsRequest, env: NodeJS.ProcessEnv = process.env): TtsResult {
    const outputPath = createTtsOutputPath('.wav', env);
    const pythonBin = env.NANOCLAW_QWEN_TTS_PYTHON_BIN || env.NANOCLAW_PYTHON_BIN || 'python3';
    const model = env.NANOCLAW_QWEN_TTS_MODEL || 'Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice';
    const mode = (env.NANOCLAW_QWEN_TTS_MODE || 'custom_voice').trim().toLowerCase();
    const language = (env.NANOCLAW_QWEN_TTS_LANGUAGE || 'Auto').trim();
    const speaker = (request.voice || env.NANOCLAW_QWEN_TTS_DEFAULT_SPEAKER || 'Vivian').trim();
    const instruct = (env.NANOCLAW_QWEN_TTS_INSTRUCT || '').trim();
    const deviceMap = (env.NANOCLAW_QWEN_TTS_DEVICE_MAP || 'auto').trim();
    const dtype = (env.NANOCLAW_QWEN_TTS_DTYPE || 'auto').trim();
    const attnImplementation = (env.NANOCLAW_QWEN_TTS_ATTN_IMPLEMENTATION || 'sdpa').trim();
    const cacheDir = (env.NANOCLAW_QWEN_TTS_CACHE_DIR || env.HF_HOME || '').trim();

    execFileSync(
      pythonBin,
      ['-c', QWEN_TTS_PYTHON_SCRIPT, JSON.stringify({
        text: request.text,
        outputPath,
        model,
        mode,
        language,
        speaker,
        instruct,
        deviceMap,
        dtype,
        attnImplementation,
        cacheDir,
      })],
      { stdio: 'pipe' },
    );

    if (!fs.existsSync(outputPath) || fs.statSync(outputPath).size === 0) {
      throw new Error(`TTS provider ${this.id} produced no audio output`);
    }

    return {
      provider: this.id,
      mode: this.mode,
      filePath: outputPath,
      mediaType: 'audio/wav',
    };
  }
}

const QWEN_TTS_PYTHON_SCRIPT = String.raw`
import json
import os
import sys

payload = json.loads(sys.argv[1])

try:
    import torch
    import soundfile as sf
    from qwen_tts import Qwen3TTSModel
except Exception as exc:
    raise RuntimeError(f"Failed to import qwen-tts dependencies: {exc}") from exc

dtype_name = (payload.get("dtype") or "auto").lower()
if dtype_name == "auto":
    if torch.cuda.is_available():
        dtype = torch.bfloat16
    else:
        dtype = torch.float32
elif dtype_name in ("bfloat16", "bf16"):
    dtype = torch.bfloat16
elif dtype_name in ("float16", "fp16", "half"):
    dtype = torch.float16
elif dtype_name in ("float32", "fp32"):
    dtype = torch.float32
else:
    raise RuntimeError(f"Unsupported NANOCLAW_QWEN_TTS_DTYPE: {payload.get('dtype')}")

kwargs = {
    "device_map": payload["deviceMap"],
    "dtype": dtype,
}
if payload.get("attnImplementation"):
    kwargs["attn_implementation"] = payload["attnImplementation"]
if payload.get("cacheDir"):
    kwargs["cache_dir"] = payload["cacheDir"]

model = Qwen3TTSModel.from_pretrained(payload["model"], **kwargs)
language = payload.get("language") or "Auto"
if language.lower() == "auto":
    language = None

mode = payload.get("mode") or "custom_voice"
text = payload["text"]
instruct = payload.get("instruct") or None

if mode == "custom_voice":
    wavs, sr = model.generate_custom_voice(
        text=text,
        language=language,
        speaker=payload["speaker"],
        instruct=instruct,
    )
elif mode == "voice_design":
    wavs, sr = model.generate_voice_design(
        text=text,
        language=language,
        instruct=instruct,
    )
else:
    raise RuntimeError(f"Unsupported NANOCLAW_QWEN_TTS_MODE: {mode}")

os.makedirs(os.path.dirname(payload["outputPath"]), exist_ok=True)
sf.write(payload["outputPath"], wavs[0], sr)
`;

const providers = new Map<string, TtsProvider>([
  ['edge', new EdgeTtsProvider()],
  ['qwen', new QwenTtsProvider()],
]);

export function resolveTtsProvider(providerId?: string, env: NodeJS.ProcessEnv = process.env): TtsProvider {
  const requested = (providerId || env.NANOCLAW_TTS_DEFAULT_PROVIDER || DEFAULT_TTS_PROVIDER).trim().toLowerCase();
  const provider = providers.get(requested);
  if (!provider) {
    throw new Error(`Unsupported TTS provider: ${requested}`);
  }
  return provider;
}

export function synthesizeSpeech(
  request: TtsRequest,
  env: NodeJS.ProcessEnv = process.env,
): TtsResult {
  const text = request.text.trim();
  if (!text) {
    throw new Error('TTS text must not be empty');
  }
  if (text.length > MAX_TTS_TEXT_LENGTH) {
    throw new Error(`TTS text exceeds ${MAX_TTS_TEXT_LENGTH} characters`);
  }

  const provider = resolveTtsProvider(request.provider, env);
  try {
    return provider.synthesize({ ...request, text }, env);
  } catch (err) {
    const outboundDir = getTtsOutboundDir(env);
    if (!fs.existsSync(outboundDir)) {
      throw err;
    }
    for (const entry of fs.readdirSync(outboundDir, { withFileTypes: true })) {
      if (!entry.isDirectory() || !entry.name.startsWith('speech-')) {
        continue;
      }
      const candidate = path.join(outboundDir, entry.name);
      try {
        if (fs.readdirSync(candidate).length === 0) {
          fs.rmSync(candidate, { recursive: true, force: true });
        }
      } catch {
        // Ignore cleanup failures after TTS errors.
      }
    }
    throw err;
  }
}
