import { randomUUID } from 'crypto';
import { basename, dirname, extname, resolve } from 'path';
import { UPLOADS_DIR } from './runtime-paths.mjs';
import { db, createImageEditVersion, getImageEditRoot } from './db.mjs';
import { renderImageVersion, resolveEditableImage } from './image-editor.mjs';

const uploadRoot = resolve(UPLOADS_DIR);
const TERMINAL_ITEM = new Set(['completed', 'failed', 'cancelled']);
const TERMINAL_JOB = new Set(['completed', 'partial', 'failed', 'cancelled']);
let loopTimer = null;
let processing = false;

function jsonParse(value, fallback = {}) {
  try { return JSON.parse(value || ''); } catch { return fallback; }
}

function normalizePath(value) {
  return String(value || '').replace(/\\/g, '/').replace(/^\/+/, '').trim();
}

function clamp(value, min, max, fallback = min) {
  const number = Number(value);
  return Number.isFinite(number) ? Math.max(min, Math.min(max, number)) : fallback;
}

function slug(value, fallback = 'bild') {
  const clean = String(value || '').normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
    .replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80);
  return clean || fallback;
}

function parseProfile(row) {
  if (!row) return null;
  return {
    ...row,
    is_builtin: Number(row.is_builtin || 0) === 1,
    operations: jsonParse(row.operations_json, {}),
  };
}

function parseItem(row) {
  if (!row) return null;
  return { ...row, settings: jsonParse(row.settings_json, {}) };
}

function parseJob(row, { includeItems = false } = {}) {
  if (!row) return null;
  const job = {
    ...row,
    pause_requested: Number(row.pause_requested || 0) === 1,
    cancel_requested: Number(row.cancel_requested || 0) === 1,
    settings: jsonParse(row.settings_json, {}),
  };
  if (includeItems) job.items = db.prepare('SELECT * FROM image_batch_job_items WHERE job_id = ? ORDER BY created_at, id').all(job.id).map(parseItem);
  return job;
}

export function listImageBatchProfiles() {
  return db.prepare('SELECT * FROM image_batch_profiles ORDER BY is_builtin DESC, name COLLATE NOCASE').all().map(parseProfile);
}

export function createImageBatchProfile(data = {}) {
  const name = String(data.name || '').trim().slice(0, 80);
  if (!name) throw new Error('Profilname fehlt.');
  const description = String(data.description || '').trim().slice(0, 300);
  const operations = data.operations && typeof data.operations === 'object' ? data.operations : {};
  const filenamePattern = String(data.filename_pattern || '{name}-{profile}-{index}').trim().slice(0, 120) || '{name}-{profile}-{index}';
  const key = `custom-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
  const result = db.prepare(`INSERT INTO image_batch_profiles
    (key, name, description, operations_json, filename_pattern, is_builtin) VALUES (?, ?, ?, ?, ?, 0)`)
    .run(key, name, description, JSON.stringify(operations), filenamePattern);
  return parseProfile(db.prepare('SELECT * FROM image_batch_profiles WHERE id = ?').get(result.lastInsertRowid));
}

export function updateImageBatchProfile(id, data = {}) {
  const current = parseProfile(db.prepare('SELECT * FROM image_batch_profiles WHERE id = ?').get(Number(id)));
  if (!current) throw new Error('Produktionsprofil wurde nicht gefunden.');
  const name = String(data.name ?? current.name).trim().slice(0, 80);
  if (!name) throw new Error('Profilname fehlt.');
  const description = String(data.description ?? current.description ?? '').trim().slice(0, 300);
  const operations = data.operations && typeof data.operations === 'object' ? data.operations : current.operations;
  const filenamePattern = String(data.filename_pattern ?? current.filename_pattern ?? '{name}-{profile}-{index}').trim().slice(0, 120);
  db.prepare(`UPDATE image_batch_profiles SET name = ?, description = ?, operations_json = ?, filename_pattern = ?, updated_at = datetime('now') WHERE id = ?`)
    .run(name, description, JSON.stringify(operations), filenamePattern, Number(id));
  return parseProfile(db.prepare('SELECT * FROM image_batch_profiles WHERE id = ?').get(Number(id)));
}

export function deleteImageBatchProfile(id) {
  const profile = parseProfile(db.prepare('SELECT * FROM image_batch_profiles WHERE id = ?').get(Number(id)));
  if (!profile) return { ok: true, deleted: 0 };
  if (profile.is_builtin) throw new Error('Integrierte Produktionsprofile können nicht gelöscht werden.');
  const inUse = Number(db.prepare("SELECT COUNT(*) AS count FROM image_batch_job_items WHERE profile_id = ? AND status IN ('queued','running')").get(Number(id))?.count || 0);
  if (inUse) throw new Error('Das Profil wird noch von einem laufenden Auftrag verwendet.');
  const result = db.prepare('DELETE FROM image_batch_profiles WHERE id = ?').run(Number(id));
  return { ok: true, deleted: result.changes };
}

function mergeOperations(profileOperations = {}, overrides = {}) {
  const operations = { ...profileOperations };
  operations.resize = { ...(profileOperations.resize || {}) };
  operations.watermark = { ...(profileOperations.watermark || {}) };
  if (overrides.remove_background === true) {
    operations.remove_background = true;
    operations.background_sensitivity = Math.round(clamp(overrides.background_sensitivity, 0, 100, 45));
    if (!overrides.format) operations.format = 'png';
  }
  if (overrides.watermark?.enabled) {
    operations.watermark = {
      enabled: true,
      text: String(overrides.watermark.text || 'Vendoo').slice(0, 80),
      position: String(overrides.watermark.position || 'bottom-right'),
      opacity: Math.round(clamp(overrides.watermark.opacity, 10, 100, 40)),
    };
  }
  if (overrides.format) operations.format = String(overrides.format);
  if (Number.isFinite(Number(overrides.quality))) operations.quality = Math.round(clamp(overrides.quality, 40, 100, 90));
  return operations;
}

function filenameFromPattern(pattern, sourcePath, profile, index) {
  const sourceName = basename(sourcePath, extname(sourcePath));
  return slug(String(pattern || '{name}-{profile}-{index}')
    .replaceAll('{name}', slug(sourceName))
    .replaceAll('{profile}', slug(profile.key || profile.name))
    .replaceAll('{index}', String(index).padStart(2, '0')),
  `vendoo-${index}`);
}

export function createImageBatchJob(data = {}) {
  const sourcePaths = [...new Set((Array.isArray(data.source_paths) ? data.source_paths : []).map(normalizePath).filter(Boolean))].slice(0, 200);
  const profileIds = [...new Set((Array.isArray(data.profile_ids) ? data.profile_ids : []).map(Number).filter(Number.isFinite))].slice(0, 20);
  if (!sourcePaths.length) throw new Error('Mindestens ein Quellbild auswählen.');
  if (!profileIds.length) throw new Error('Mindestens ein Produktionsprofil auswählen.');
  for (const sourcePath of sourcePaths) resolveEditableImage(uploadRoot, sourcePath);
  const placeholders = profileIds.map(() => '?').join(',');
  const profiles = db.prepare(`SELECT * FROM image_batch_profiles WHERE id IN (${placeholders}) ORDER BY is_builtin DESC, name`).all(...profileIds).map(parseProfile);
  if (profiles.length !== profileIds.length) throw new Error('Mindestens ein Produktionsprofil wurde nicht gefunden.');

  const id = randomUUID();
  const name = String(data.name || `Bildproduktion ${new Date().toLocaleDateString('de-DE')}`).trim().slice(0, 100);
  const overrides = data.overrides && typeof data.overrides === 'object' ? data.overrides : {};
  const settings = { source_paths: sourcePaths, profile_ids: profileIds, overrides };
  const totalItems = sourcePaths.length * profiles.length;
  const insertJob = db.prepare(`INSERT INTO image_batch_jobs
    (id, name, source_count, profile_count, total_items, settings_json) VALUES (?, ?, ?, ?, ?, ?)`);
  const insertItem = db.prepare(`INSERT INTO image_batch_job_items
    (id, job_id, source_path, profile_id, profile_key, profile_name, settings_json) VALUES (?, ?, ?, ?, ?, ?, ?)`);
  const transaction = db.transaction(() => {
    insertJob.run(id, name, sourcePaths.length, profiles.length, totalItems, JSON.stringify(settings));
    let itemIndex = 0;
    for (const sourcePath of sourcePaths) {
      for (const profile of profiles) {
        itemIndex += 1;
        const operations = mergeOperations(profile.operations, overrides);
        const itemSettings = {
          operations,
          filename_pattern: profile.filename_pattern,
          output_filename: filenameFromPattern(profile.filename_pattern, sourcePath, profile, itemIndex),
        };
        insertItem.run(randomUUID(), id, sourcePath, profile.id, profile.key, profile.name, JSON.stringify(itemSettings));
      }
    }
  });
  transaction();
  scheduleLoop(20);
  return getImageBatchJob(id);
}

export function getImageBatchJob(id) {
  return parseJob(db.prepare('SELECT * FROM image_batch_jobs WHERE id = ?').get(String(id || '')), { includeItems: true });
}

export function paginateImageBatchJobs({ page = 1, pageSize = 10, status = '' } = {}) {
  const safeSize = Math.max(5, Math.min(15, Number(pageSize) || 10));
  const requestedPage = Math.max(1, Number(page) || 1);
  const statuses = String(status || '').split(',').map(value => value.trim()).filter(Boolean);
  const where = statuses.length ? `WHERE status IN (${statuses.map(() => '?').join(',')})` : '';
  const total = Number(db.prepare(`SELECT COUNT(*) AS count FROM image_batch_jobs ${where}`).get(...statuses)?.count || 0);
  const totalPages = Math.max(1, Math.ceil(total / safeSize));
  const safePage = Math.min(requestedPage, totalPages);
  const rows = db.prepare(`SELECT * FROM image_batch_jobs ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`)
    .all(...statuses, safeSize, (safePage - 1) * safeSize).map(row => parseJob(row));
  return { items: rows, pagination: { page: safePage, page_size: safeSize, total, total_pages: totalPages }, summary: getImageBatchSummary() };
}

export function getImageBatchSummary() {
  const rows = db.prepare('SELECT status, COUNT(*) AS count FROM image_batch_jobs GROUP BY status').all();
  const summary = { total: 0, queued: 0, running: 0, paused: 0, completed: 0, partial: 0, failed: 0, cancelled: 0 };
  for (const row of rows) { summary[row.status] = Number(row.count || 0); summary.total += Number(row.count || 0); }
  return summary;
}

function refreshJob(jobId) {
  const counts = db.prepare(`SELECT
    COUNT(*) AS total,
    SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
    SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
    SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) AS cancelled,
    SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) AS running,
    SUM(CASE WHEN status = 'queued' THEN 1 ELSE 0 END) AS queued
    FROM image_batch_job_items WHERE job_id = ?`).get(jobId);
  const job = db.prepare('SELECT * FROM image_batch_jobs WHERE id = ?').get(jobId);
  if (!job) return null;
  let status = job.status;
  let phase = job.phase;
  let completedAt = job.completed_at;
  if (Number(job.cancel_requested || 0) === 1 && Number(counts.running || 0) === 0) {
    status = 'cancelled'; phase = 'cancelled'; completedAt = completedAt || new Date().toISOString();
  } else if (Number(job.pause_requested || 0) === 1 && Number(counts.running || 0) === 0) {
    status = 'paused'; phase = 'paused';
  } else if (Number(counts.running || 0) > 0) {
    status = 'running'; phase = 'processing';
  } else if (Number(counts.queued || 0) > 0) {
    status = 'queued'; phase = 'queued';
  } else if (Number(counts.total || 0) > 0) {
    const failed = Number(counts.failed || 0);
    const completed = Number(counts.completed || 0);
    const cancelled = Number(counts.cancelled || 0);
    if (completed === Number(counts.total)) status = 'completed';
    else if (completed > 0 && (failed > 0 || cancelled > 0)) status = 'partial';
    else if (failed > 0) status = 'failed';
    else status = 'cancelled';
    phase = status; completedAt = completedAt || new Date().toISOString();
  }
  db.prepare(`UPDATE image_batch_jobs SET status = ?, phase = ?, completed_items = ?, failed_items = ?, cancelled_items = ?, completed_at = ?, updated_at = datetime('now') WHERE id = ?`)
    .run(status, phase, Number(counts.completed || 0), Number(counts.failed || 0), Number(counts.cancelled || 0), completedAt, jobId);
  return getImageBatchJob(jobId);
}

export function pauseImageBatchJob(id) {
  const job = getImageBatchJob(id);
  if (!job) throw new Error('Bildauftrag wurde nicht gefunden.');
  if (TERMINAL_JOB.has(job.status)) return job;
  db.prepare("UPDATE image_batch_jobs SET pause_requested = 1, status = CASE WHEN status = 'running' THEN status ELSE 'paused' END, phase = 'pausing', updated_at = datetime('now') WHERE id = ?").run(job.id);
  return refreshJob(job.id);
}

export function resumeImageBatchJob(id) {
  const job = getImageBatchJob(id);
  if (!job) throw new Error('Bildauftrag wurde nicht gefunden.');
  if (TERMINAL_JOB.has(job.status) && !['failed', 'partial'].includes(job.status)) return job;
  db.prepare("UPDATE image_batch_jobs SET pause_requested = 0, cancel_requested = 0, status = 'queued', phase = 'queued', completed_at = NULL, updated_at = datetime('now') WHERE id = ?").run(job.id);
  scheduleLoop(20);
  return refreshJob(job.id);
}

export function cancelImageBatchJob(id) {
  const job = getImageBatchJob(id);
  if (!job) throw new Error('Bildauftrag wurde nicht gefunden.');
  if (TERMINAL_JOB.has(job.status)) return job;
  const tx = db.transaction(() => {
    db.prepare("UPDATE image_batch_jobs SET cancel_requested = 1, pause_requested = 0, phase = 'cancelling', updated_at = datetime('now') WHERE id = ?").run(job.id);
    db.prepare("UPDATE image_batch_job_items SET status = 'cancelled', phase = 'cancelled', completed_at = datetime('now'), updated_at = datetime('now') WHERE job_id = ? AND status = 'queued'").run(job.id);
  });
  tx();
  return refreshJob(job.id);
}

export function retryImageBatchJob(id) {
  const job = getImageBatchJob(id);
  if (!job) throw new Error('Bildauftrag wurde nicht gefunden.');
  const result = db.prepare("UPDATE image_batch_job_items SET status = 'queued', phase = 'queued', error_message = NULL, output_path = NULL, started_at = NULL, completed_at = NULL, updated_at = datetime('now') WHERE job_id = ? AND status IN ('failed','cancelled')").run(job.id);
  if (!result.changes) throw new Error('Dieser Auftrag enthält keine wiederholbaren Positionen.');
  db.prepare("UPDATE image_batch_jobs SET status = 'queued', phase = 'queued', pause_requested = 0, cancel_requested = 0, error_message = NULL, completed_at = NULL, updated_at = datetime('now') WHERE id = ?").run(job.id);
  scheduleLoop(20);
  return refreshJob(job.id);
}

export function retryImageBatchItem(jobId, itemId) {
  const item = parseItem(db.prepare('SELECT * FROM image_batch_job_items WHERE id = ? AND job_id = ?').get(String(itemId), String(jobId)));
  if (!item) throw new Error('Bildposition wurde nicht gefunden.');
  if (!['failed', 'cancelled'].includes(item.status)) throw new Error('Nur fehlgeschlagene oder abgebrochene Positionen können wiederholt werden.');
  db.prepare("UPDATE image_batch_job_items SET status = 'queued', phase = 'queued', error_message = NULL, output_path = NULL, started_at = NULL, completed_at = NULL, updated_at = datetime('now') WHERE id = ?").run(item.id);
  db.prepare("UPDATE image_batch_jobs SET status = 'queued', phase = 'queued', pause_requested = 0, cancel_requested = 0, completed_at = NULL, updated_at = datetime('now') WHERE id = ?").run(jobId);
  scheduleLoop(20);
  return refreshJob(jobId);
}

export function deleteImageBatchJob(id) {
  const job = getImageBatchJob(id);
  if (!job) return { ok: true, deleted: 0 };
  if (!TERMINAL_JOB.has(job.status)) throw new Error('Laufende Bildaufträge können nicht gelöscht werden.');
  const result = db.prepare('DELETE FROM image_batch_jobs WHERE id = ?').run(job.id);
  return { ok: true, deleted: result.changes };
}

export function getImageBatchOutputPaths(id) {
  return db.prepare("SELECT output_path FROM image_batch_job_items WHERE job_id = ? AND status = 'completed' AND output_path IS NOT NULL ORDER BY created_at, id")
    .all(String(id || '')).map(row => row.output_path).filter(Boolean);
}

async function processNextItem() {
  if (processing) return;
  const item = parseItem(db.prepare(`SELECT i.* FROM image_batch_job_items i
    JOIN image_batch_jobs j ON j.id = i.job_id
    WHERE i.status = 'queued' AND j.pause_requested = 0 AND j.cancel_requested = 0 AND j.status IN ('queued','running')
    ORDER BY j.created_at ASC, i.created_at ASC LIMIT 1`).get());
  if (!item) return;
  processing = true;
  try {
    const job = db.prepare('SELECT * FROM image_batch_jobs WHERE id = ?').get(item.job_id);
    db.prepare("UPDATE image_batch_jobs SET status = 'running', phase = 'processing', started_at = COALESCE(started_at, datetime('now')), updated_at = datetime('now') WHERE id = ?").run(item.job_id);
    db.prepare("UPDATE image_batch_job_items SET status = 'running', phase = 'rendering', started_at = datetime('now'), updated_at = datetime('now') WHERE id = ?").run(item.id);
    const outputDirectory = `batch/${String(item.job_id).slice(0, 8)}`;
    const rootPath = getImageEditRoot(item.source_path) || item.source_path;
    const result = await renderImageVersion({
      uploadRoot,
      sourcePath: item.source_path,
      operations: item.settings.operations || {},
      context: 'batch',
      rootPath,
      recordVersion: createImageEditVersion,
      outputDirectory,
      outputFilename: item.settings.output_filename,
    });
    db.prepare("UPDATE image_batch_job_items SET status = 'completed', phase = 'completed', output_path = ?, completed_at = datetime('now'), updated_at = datetime('now') WHERE id = ?")
      .run(result.filename, item.id);
  } catch (error) {
    db.prepare("UPDATE image_batch_job_items SET status = 'failed', phase = 'failed', error_message = ?, completed_at = datetime('now'), updated_at = datetime('now') WHERE id = ?")
      .run(String(error?.message || error || 'Bild konnte nicht verarbeitet werden.').slice(0, 1000), item.id);
  } finally {
    refreshJob(item.job_id);
    processing = false;
    scheduleLoop(20);
  }
}

function scheduleLoop(delay = 500) {
  if (loopTimer) clearTimeout(loopTimer);
  loopTimer = setTimeout(async () => {
    loopTimer = null;
    await processNextItem();
    scheduleLoop(500);
  }, delay);
  loopTimer.unref?.();
}

export function stopImageBatchJobLoop() {
  if (loopTimer) clearTimeout(loopTimer);
  loopTimer = null;
}

export function startImageBatchJobLoop() {
  db.prepare("UPDATE image_batch_job_items SET status = 'queued', phase = 'queued', started_at = NULL, updated_at = datetime('now') WHERE status = 'running'").run();
  db.prepare("UPDATE image_batch_job_items SET status = 'cancelled', phase = 'cancelled', completed_at = COALESCE(completed_at, datetime('now')), updated_at = datetime('now') WHERE status = 'queued' AND job_id IN (SELECT id FROM image_batch_jobs WHERE cancel_requested = 1)").run();
  db.prepare("UPDATE image_batch_jobs SET status = CASE WHEN cancel_requested = 1 THEN 'cancelled' WHEN pause_requested = 1 THEN 'paused' ELSE 'queued' END, phase = CASE WHEN cancel_requested = 1 THEN 'cancelled' WHEN pause_requested = 1 THEN 'paused' ELSE 'queued' END, updated_at = datetime('now') WHERE status = 'running'").run();
  for (const row of db.prepare("SELECT id FROM image_batch_jobs WHERE status IN ('queued','running','paused','cancelled')").all()) refreshJob(row.id);
  scheduleLoop(200);
}
