import type { ServerWebSocket } from 'bun';
import { spawn, type Subprocess } from 'bun';
import { resolve, normalize, dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { getOwnerHomeDir } from '@@/data-path';

const __dirname = dirname(fileURLToPath(import.meta.url));
const ASOUNDRC_PATH = join(__dirname, 'asoundrc');

type WSData = {
  userId: number;
  email: string;
  username: string;
  files: string;
};

type CliampSession = {
  proc: Subprocess<'pipe', 'pipe', 'pipe'>;
  closed: boolean;
};

const sessions = new Map<ServerWebSocket<WSData>, CliampSession>();

const sendOutput = (ws: ServerWebSocket<WSData>, data: string) => {
  try {
    ws.send(JSON.stringify({ type: 'output', data }));
  } catch {
    // ws already closed
  }
};

const sendExit = (ws: ServerWebSocket<WSData>) => {
  try {
    ws.send(JSON.stringify({ type: 'exit' }));
  } catch {
    // ws already closed
  }
};

const validateFilePaths = (files: string[], homeDir: string): string[] | null => {
  const resolved = files.map((f) => normalize(resolve(homeDir, f)));
  for (const p of resolved) {
    if (!p.startsWith(homeDir)) return null;
  }
  return resolved;
};

const findCliamp = (): string | null => {
  const which = Bun.which('cliamp');
  if (which) return which;
  const candidates = [
    process.env.GOPATH ? `${process.env.GOPATH}/bin/cliamp` : null,
    `${process.env.HOME}/.local/go-path/bin/cliamp`,
    `${process.env.HOME}/go/bin/cliamp`,
  ];
  for (const bin of candidates) {
    if (!bin) continue;
    try {
      const stat = Bun.spawnSync({ cmd: ['test', '-x', bin], stdout: 'ignore', stderr: 'ignore' });
      if (stat.exitCode === 0) return bin;
    } catch {
      /* ignore */
    }
  }
  return null;
};

const shellEscape = (s: string) => `'${s.replace(/'/g, "'\\''")}'`;

export const cliampWebsocket = {
  async open(ws: ServerWebSocket<WSData>) {
    const { email, files: filesParam } = ws.data;

    if (!filesParam) {
      sendOutput(ws, '\r\n[Error] No files specified.\r\n');
      return;
    }

    const cliampPath = findCliamp();
    if (!cliampPath) {
      sendOutput(ws, '\r\n[Error] cliamp not found on host.\r\n');
      return;
    }

    const homeDir = getOwnerHomeDir(email);
    const rawFiles = [filesParam];

    // Resolve paths relative to user home dir
    const absoluteFiles = rawFiles.map((f) => {
      if (f.startsWith('/')) return `${homeDir}${f}`;
      return `${homeDir}/${f}`;
    });

    const validated = validateFilePaths(absoluteFiles, homeDir);
    if (!validated) {
      sendOutput(ws, '\r\n[Error] Invalid file path.\r\n');
      return;
    }

    // Use `script` to allocate a PTY for cliamp (avoids node-pty dependency)
    // script -qfc '<command> <args...>' /dev/null
    const cliampCmd = [shellEscape(cliampPath), ...validated.map(shellEscape)].join(' ');
    console.log(`[cliamp] spawning: ${cliampCmd}`);
    let proc: Subprocess<'pipe', 'pipe', 'pipe'>;
    try {
      proc = spawn({
        cmd: ['script', '-qfc', cliampCmd, '/dev/null'],
        stdin: 'pipe',
        stdout: 'pipe',
        stderr: 'pipe',
        cwd: homeDir,
        env: { ...process.env, TERM: 'xterm-256color', PULSE_SINK: 'virtual_out', ALSA_CONFIG_PATH: ASOUNDRC_PATH },
      });
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Failed to start cliamp';
      sendOutput(ws, `\r\n[Error] ${message}\r\n`);
      return;
    }

    const session: CliampSession = { proc, closed: false };
    sessions.set(ws, session);

    // Pump stdout → WS
    const reader = proc.stdout.getReader();
    const pump = async () => {
      try {
        while (!session.closed) {
          const { done, value } = await reader.read();
          if (done) break;
          if (value && !session.closed) {
            sendOutput(ws, new TextDecoder().decode(value));
          }
        }
      } catch {
        // stream ended
      } finally {
        if (!session.closed) {
          session.closed = true;
          sendExit(ws);
        }
      }
    };
    pump();

    // Also read stderr (cliamp may write there)
    const stderrReader = proc.stderr.getReader();
    const pumpStderr = async () => {
      try {
        while (!session.closed) {
          const { done, value } = await stderrReader.read();
          if (done) break;
          if (value && !session.closed) {
            sendOutput(ws, new TextDecoder().decode(value));
          }
        }
      } catch {
        // stream ended
      }
    };
    pumpStderr();

    // On process exit → notify client
    proc.exited.then((code) => {
      console.log(`[cliamp] process exited code=${code}`);
      if (!session.closed) {
        session.closed = true;
        sendExit(ws);
      }
      sessions.delete(ws);
    });
  },

  message(ws: ServerWebSocket<WSData>, raw: string | Buffer) {
    const session = sessions.get(ws);
    if (!session || session.closed) return;

    try {
      const msg = JSON.parse(typeof raw === 'string' ? raw : raw.toString());
      if (msg.type === 'input' && msg.data) {
        session.proc.stdin.write(msg.data);
      }
    } catch {
      // ignore
    }
  },

  close(ws: ServerWebSocket<WSData>) {
    const session = sessions.get(ws);
    if (session) {
      session.closed = true;
      try {
        session.proc.kill();
      } catch {
        /* ignore */
      }
      sessions.delete(ws);
    }
  },

  drain() {},
};
