/**
 * Cron Utility
 * Wrapper around node-cron for scheduling tasks
 *
 * Usage in modules:
 * import { schedule, validate } from '@zen/core/cron';
 */

import cron from 'node-cron';
import { done, fail, info } from '@zen/core/shared/logger';

// Shared store — survives Next.js hot-reload and module-cache invalidation
const CRON_JOBS_KEY = Symbol.for('__ZEN_CRON_JOBS__');

/**
 * @typedef {Object} CronEntry
 * @property {Object}   job          - node-cron task instance
 * @property {Function} handler      - original handler function
 * @property {string}   schedule     - cron expression
 * @property {string}   timezone     - timezone used
 * @property {string}   registeredAt - ISO timestamp of registration
 */

/**
 * @returns {Map<string, CronEntry>}
 */
function getJobsStorage() {
  if (!globalThis[CRON_JOBS_KEY]) {
    globalThis[CRON_JOBS_KEY] = new Map();
  }
  return globalThis[CRON_JOBS_KEY];
}

/**
 * Schedule a cron job.
 *
 * If a job with the same name already exists it is stopped and replaced.
 *
 * @param {string}   name         - Unique name for the job
 * @param {string}   cronSchedule - Cron expression (5 or 6 fields)
 * @param {Function} handler      - Async function to execute
 * @param {Object}   [options]
 * @param {string}   [options.timezone]   - IANA timezone (default: ZEN_TIMEZONE env or America/Toronto)
 * @param {boolean}  [options.runOnInit]  - Run immediately when scheduled (default: false)
 * @returns {Object} node-cron task instance
 *
 * @example
 * schedule('daily-report', '0 9 * * *', async () => {
 *   await sendReport();
 * });
 *
 * @example
 * schedule('every-5min', '*\/5 * * * *', async () => {
 *   await syncData();
 * }, { timezone: 'America/New_York' });
 */
export function schedule(name, cronSchedule, handler, options = {}) {
  if (!name || typeof name !== 'string') {
    throw new Error('Cron job name must be a non-empty string');
  }
  if (!validate(cronSchedule)) {
    throw new Error(`Invalid cron expression: "${cronSchedule}"`);
  }
  if (typeof handler !== 'function') {
    throw new Error('Cron job handler must be a function');
  }

  const jobs = getJobsStorage();

  // Replace existing job with same name
  if (jobs.has(name)) {
    jobs.get(name).job.stop();
    info(`Cron replaced: ${name}`);
  }

  const timezone = options.timezone || process.env.ZEN_TIMEZONE || 'America/Toronto';

  const job = cron.schedule(cronSchedule, async () => {
    try {
      await handler();
    } catch (error) {
      fail(`Cron ${name}: ${error.message}`);
    }
  }, { timezone });

  if (options.runOnInit) {
    handler().catch((error) => fail(`Cron ${name}: ${error.message}`));
  }

  jobs.set(name, {
    job,
    handler,
    schedule: cronSchedule,
    timezone,
    registeredAt: new Date().toISOString()
  });

  done(`Cron scheduled: ${name} (${cronSchedule})`);

  return job;
}

/**
 * Stop and remove a scheduled cron job.
 *
 * @param {string} name - Job name
 * @returns {boolean} True if the job existed and was stopped
 */
export function stop(name) {
  const jobs = getJobsStorage();

  if (jobs.has(name)) {
    jobs.get(name).job.stop();
    jobs.delete(name);
    info(`Cron stopped: ${name}`);
    return true;
  }

  return false;
}

/**
 * Stop and remove all scheduled cron jobs.
 */
export function stopAll() {
  const jobs = getJobsStorage();

  for (const [name, entry] of jobs.entries()) {
    entry.job.stop();
    info(`Cron stopped: ${name}`);
  }

  jobs.clear();
}

/**
 * Manually trigger a job by name, bypassing its schedule.
 *
 * @param {string} name - Job name
 * @returns {Promise<void>}
 * @throws {Error} If the job does not exist
 */
export async function trigger(name) {
  const jobs = getJobsStorage();

  if (!jobs.has(name)) {
    throw new Error(`Cron job '${name}' not found`);
  }

  info(`Cron manual trigger: ${name}`);
  await jobs.get(name).handler();
}

/**
 * Validate a cron expression.
 *
 * @param {string} expression
 * @returns {boolean}
 */
export function validate(expression) {
  return cron.validate(expression);
}

/**
 * Check whether a job is currently scheduled.
 *
 * @param {string} name - Job name
 * @returns {boolean}
 */
export function isRunning(name) {
  return getJobsStorage().has(name);
}

/**
 * Return the names of all scheduled jobs.
 *
 * @returns {string[]}
 */
export function getJobs() {
  return Array.from(getJobsStorage().keys());
}

/**
 * Return metadata for all scheduled jobs.
 *
 * @returns {Object.<string, { schedule: string, timezone: string, registeredAt: string }>}
 */
export function getStatus() {
  const status = {};

  for (const [name, entry] of getJobsStorage().entries()) {
    status[name] = {
      schedule: entry.schedule,
      timezone: entry.timezone,
      registeredAt: entry.registeredAt
    };
  }

  return status;
}

export default {
  schedule,
  stop,
  stopAll,
  trigger,
  validate,
  isRunning,
  getJobs,
  getStatus
};
