// backend/src/services/codex-image.js
// Per-client Codex CLI image generation.
// Uses per-user CODEX_HOME so each client's ChatGPT auth is completely isolated.
// Never uses OPENAI_API_KEY or CODEX_API_KEY.

import { execFile } from 'child_process';
import { promisify } from 'util';
import fs from 'fs/promises';
import path from 'path';
import { encrypt, decrypt } from '../utils/encryption.js';
import { query } from '../utils/db.js';
import { logger } from '../utils/logger.js';

const execFileAsync = promisify(execFile);

const CODEX_COMMAND    = process.env.CODEX_COMMAND    || 'codex';
const CODEX_WORKDIR    = process.env.CODEX_WORKDIR    || 'storage/app/codex-images';
const CODEX_HOME_BASE  = process.env.CODEX_HOME_BASE  || 'storage/app/codex-home';
const CODEX_TIMEOUT_MS = parseInt(process.env.CODEX_TIMEOUT_MS || '180000', 10);

// Resolve to absolute path relative to project root
const PROJECT_ROOT = path.resolve(process.cwd(), '..');
function absPath(rel) {
  return path.isAbsolute(rel) ? rel : path.join(PROJECT_ROOT, rel);
}

// ─── Path helpers ────────────────────────────────────────────────────────────

function userWorkdir(userId) {
  return absPath(path.join(CODEX_WORKDIR, userId));
}

function userCodexHome(userId) {
  return absPath(path.join(CODEX_HOME_BASE, userId));
}

function authJsonPath(userId) {
  return path.join(userCodexHome(userId), 'auth.json');
}

// ─── Directory setup ─────────────────────────────────────────────────────────

async function ensureDirs(userId) {
  await fs.mkdir(userWorkdir(userId),   { recursive: true, mode: 0o750 });
  await fs.mkdir(userCodexHome(userId), { recursive: true, mode: 0o700 });
}

// ─── Auth materialization ────────────────────────────────────────────────────

async function materializeAuth(userId) {
  const { rows } = await query(
    'SELECT encrypted_auth FROM codex_credentials WHERE user_id = $1 AND status = $2',
    [userId, 'connected']
  );
  if (!rows[0]) throw new CodexError('NOT_CONNECTED', 'ChatGPT not connected. Go to Settings → ChatGPT.');

  const raw = decrypt(rows[0].encrypted_auth);
  const authPath = authJsonPath(userId);
  await fs.writeFile(authPath, raw, { encoding: 'utf8', mode: 0o600 });
  logger.debug(`Materialized auth.json for user ${userId}`);
}

async function persistAuth(userId) {
  try {
    const authPath = authJsonPath(userId);
    const raw = await fs.readFile(authPath, 'utf8');
    JSON.parse(raw); // Validate it's still valid JSON
    const encrypted = encrypt(raw);
    await query(
      `UPDATE codex_credentials
         SET encrypted_auth = $1, last_refreshed_at = NOW()
       WHERE user_id = $2`,
      [encrypted, userId]
    );
    logger.debug(`Persisted refreshed auth.json for user ${userId}`);
  } catch (err) {
    logger.warn(`Could not persist auth for user ${userId}: ${err.message}`);
  }
}

async function cleanupAuth(userId) {
  try {
    const authPath = authJsonPath(userId);
    await fs.unlink(authPath);
  } catch {}
}

// ─── Login status check ──────────────────────────────────────────────────────

async function checkLoginStatus(userId) {
  const codexHome = userCodexHome(userId);
  const env = buildCleanEnv({ CODEX_HOME: codexHome });

  try {
    const { stdout, stderr } = await execFileAsync(
      CODEX_COMMAND,
      ['login', 'status'],
      { env, timeout: 10000 }
    );
    const out = (stdout + stderr).toLowerCase();
    const loggedIn = out.includes('logged in') || out.includes('authenticated') || !out.includes('not logged');
    return { loggedIn, detail: stdout.trim() || stderr.trim() };
  } catch (err) {
    const msg = (err.stdout || err.stderr || err.message || '').toLowerCase();
    if (msg.includes('not logged') || msg.includes('unauthenticated') || err.code === 1) {
      return { loggedIn: false, detail: 'Not logged in' };
    }
    // Command not found or other error
    throw new CodexError('CODEX_UNAVAILABLE', `Codex CLI not available: ${err.message}`);
  }
}

// ─── Env builder — strips all API keys ──────────────────────────────────────

function buildCleanEnv(extra = {}) {
  const env = { ...process.env };
  // Explicitly delete API keys — Codex must use ChatGPT auth only
  delete env.OPENAI_API_KEY;
  delete env.CODEX_API_KEY;
  delete env.OPENAI_ORG_ID;
  // Disable API mode
  env.CODEX_USE_API = 'false';
  // Apply per-user overrides
  Object.assign(env, extra);
  return env;
}

// ─── File name sanitization ──────────────────────────────────────────────────

export function sanitizeFileName(name) {
  if (!name || typeof name !== 'string') {
    return `image_${Date.now()}.png`;
  }
  // Strip extension, sanitize chars, re-add .png
  const base = path.basename(name, path.extname(name))
    .toLowerCase()
    .replace(/[^a-z0-9_-]/g, '_')
    .replace(/_+/g, '_')
    .replace(/^_|_$/g, '')
    .slice(0, 80);
  return (base || `image_${Date.now()}`) + '.png';
}

// ─── Prompt builder ──────────────────────────────────────────────────────────

function buildPrompt(userPrompt, fileName) {
  const truncated = userPrompt.slice(0, 1800).replace(/[\x00-\x1f\x7f]/g, ' ');
  return (
    `$imagegen Generate an image based on this description: ${truncated}. ` +
    `Save the result as a PNG file named "${fileName}" in the current working directory. ` +
    `Do not open any browser or external application. ` +
    `Only write to the current directory.`
  );
}

// ─── PNG finder ──────────────────────────────────────────────────────────────

async function findGeneratedPng(workdir, expectedName, beforeMs) {
  // First: check expected name
  const expected = path.join(workdir, expectedName);
  try {
    const stat = await fs.stat(expected);
    if (stat.mtimeMs >= beforeMs - 1000) return expected;
  } catch {}

  // Fallback: find newest PNG created after the run started
  const files = await fs.readdir(workdir);
  const pngs = await Promise.all(
    files
      .filter(f => f.endsWith('.png'))
      .map(async f => {
        const fp = path.join(workdir, f);
        const s = await fs.stat(fp);
        return { fp, mtime: s.mtimeMs };
      })
  );
  const recent = pngs
    .filter(p => p.mtime >= beforeMs - 2000)
    .sort((a, b) => b.mtime - a.mtime);

  return recent[0]?.fp || null;
}

// ─── Validate path stays in workdir ─────────────────────────────────────────

function assertInWorkdir(filePath, workdir) {
  const resolved = path.resolve(filePath);
  const base     = path.resolve(workdir);
  if (!resolved.startsWith(base + path.sep) && resolved !== base) {
    throw new CodexError('PATH_TRAVERSAL', 'Generated file is outside allowed directory');
  }
}

// ─── Custom error ────────────────────────────────────────────────────────────

export class CodexError extends Error {
  constructor(code, message) {
    super(message);
    this.code = code;
    this.name = 'CodexError';
  }
}

// ─── MAIN: generate ──────────────────────────────────────────────────────────

export async function generate({ userId, prompt, fileName }) {
  if (!prompt || typeof prompt !== 'string' || prompt.trim().length < 3) {
    throw new CodexError('INVALID_PROMPT', 'Prompt must be at least 3 characters');
  }

  const safeFileName = sanitizeFileName(fileName);
  const workdir      = userWorkdir(userId);
  const codexHome    = userCodexHome(userId);

  await ensureDirs(userId);
  await materializeAuth(userId);

  const status = await checkLoginStatus(userId);
  if (!status.loggedIn) {
    await cleanupAuth(userId);
    throw new CodexError('NOT_LOGGED_IN', 'Rulează codex login pe mașina ta și re-încarcă auth.json.');
  }

  const finalPrompt = buildPrompt(prompt.trim(), safeFileName);
  const env         = buildCleanEnv({ CODEX_HOME: codexHome });
  const beforeMs    = Date.now();

  logger.info(`Starting Codex image generation for user ${userId}`, { fileName: safeFileName });

  try {
    await execFileAsync(
      CODEX_COMMAND,
      ['exec', '--cd', workdir, '--sandbox', 'workspace-write', finalPrompt],
      {
        env,
        timeout: CODEX_TIMEOUT_MS,
        maxBuffer: 10 * 1024 * 1024, // 10MB
      }
    );
  } catch (err) {
    await cleanupAuth(userId);

    if (err.killed || err.signal === 'SIGTERM') {
      throw new CodexError('TIMEOUT', `Image generation timed out after ${CODEX_TIMEOUT_MS / 1000}s`);
    }
    const msg = err.stderr || err.stdout || err.message || '';
    if (msg.toLowerCase().includes('not logged') || msg.toLowerCase().includes('auth')) {
      await query(
        "UPDATE codex_credentials SET status = 'expired' WHERE user_id = $1",
        [userId]
      );
      throw new CodexError('TOKEN_EXPIRED', 'ChatGPT session expired. Please reconnect from Settings.');
    }
    throw new CodexError('EXEC_FAILED', `Codex execution failed: ${msg.slice(0, 300)}`);
  }

  // Persist possibly-refreshed tokens back to DB
  await persistAuth(userId);
  await cleanupAuth(userId); // Remove from disk after saving to DB

  // Find the generated PNG
  const pngPath = await findGeneratedPng(workdir, safeFileName, beforeMs);
  if (!pngPath) {
    throw new CodexError('NO_OUTPUT', 'Image generation completed but no PNG file was found');
  }

  // Security: verify file is inside workdir
  assertInWorkdir(pngPath, workdir);

  // Update last_used_at
  await query(
    'UPDATE codex_credentials SET last_used_at = NOW() WHERE user_id = $1',
    [userId]
  );

  logger.info(`Image generated successfully for user ${userId}`, { path: pngPath });

  return {
    fileName: path.basename(pngPath),
    path:     pngPath,
    workdir,
  };
}

// ─── Credential management helpers (used by routes) ──────────────────────────

export async function saveCredentials(userId, authJsonString) {
  // Validate it's real JSON with expected fields
  const parsed = JSON.parse(authJsonString);
  if (!parsed || typeof parsed !== 'object') {
    throw new CodexError('INVALID_AUTH', 'Invalid auth.json format');
  }

  const encrypted = encrypt(authJsonString);
  await query(
    `INSERT INTO codex_credentials (user_id, encrypted_auth, status, connected_at)
     VALUES ($1, $2, 'connected', NOW())
     ON CONFLICT (user_id) DO UPDATE
       SET encrypted_auth = $2, status = 'connected', connected_at = NOW(),
           last_refreshed_at = NULL`,
    [userId, encrypted]
  );
}

export async function removeCredentials(userId) {
  await query('DELETE FROM codex_credentials WHERE user_id = $1', [userId]);
  // Clean up filesystem directories
  try {
    await fs.rm(userWorkdir(userId),   { recursive: true, force: true });
    await fs.rm(userCodexHome(userId), { recursive: true, force: true });
  } catch {}
}

export async function getConnectionStatus(userId) {
  const { rows } = await query(
    'SELECT status, connected_at, last_used_at, last_refreshed_at FROM codex_credentials WHERE user_id = $1',
    [userId]
  );
  if (!rows[0]) return { connected: false };
  return {
    connected:       rows[0].status === 'connected',
    status:          rows[0].status,
    connectedAt:     rows[0].connected_at,
    lastUsedAt:      rows[0].last_used_at,
    lastRefreshedAt: rows[0].last_refreshed_at,
  };
}
