/**
 * Cron Service - Scheduled tasks
 * 
 * Runs scheduled jobs that send messages to the agent.
 * Supports heartbeat check-ins and agent-managed cron jobs.
 */

import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, copyFileSync, renameSync, watch, type FSWatcher } from 'node:fs';
import { resolve, dirname } from 'node:path';
import type { AgentSession } from '../core/interfaces.js';
import type { CronJob, CronJobCreate, CronSchedule, CronConfig, HeartbeatConfig } from './types.js';
import { DEFAULT_HEARTBEAT_MESSAGES } from './types.js';
import { getCronDataDir, getCronLogPath, getCronStorePath, getLegacyCronStorePath } from '../utils/paths.js';

import { createLogger } from '../logger.js';

const log = createLogger('Cron');
// Log file for cron events
const LOG_PATH = getCronLogPath();

function logEvent(event: string, data: Record<string, unknown>): void {
  const entry = {
    timestamp: new Date().toISOString(),
    event,
    ...data,
  };
  
  try {
    mkdirSync(dirname(LOG_PATH), { recursive: true });
    appendFileSync(LOG_PATH, JSON.stringify(entry) + '\n');
  } catch {
    // Ignore log errors
  }
  
  log.info(`${event}:`, JSON.stringify(data));
}

// Dynamic import for node-schedule
let schedule: typeof import('node-schedule');

interface CronStoreFile {
  version: 1;
  jobs: CronJob[];
}

const DEFAULT_HEARTBEAT: HeartbeatConfig = {
  enabled: false,
  schedule: '0 * * * *', // Every hour
  message: DEFAULT_HEARTBEAT_MESSAGES.simple,
};

export class CronService {
  private jobs: Map<string, CronJob> = new Map();
  private scheduledJobs: Map<string, import('node-schedule').Job> = new Map();
  private bot: AgentSession;
  private storePath: string;
  private config: CronConfig;
  private started = false;
  private heartbeatJob: import('node-schedule').Job | null = null;
  private fileWatcher: FSWatcher | null = null;
  private lastFileContent: string = '';
  
  constructor(bot: AgentSession, config?: CronConfig) {
    this.bot = bot;
    this.config = config || {};
    this.storePath = config?.storePath 
      ? resolve(getCronDataDir(), config.storePath)
      : getCronStorePath();
    this.migrateLegacyStoreIfNeeded();
    this.migrateFromGlobalStoreIfNeeded();
    this.loadJobs();
  }

  private migrateLegacyStoreIfNeeded(): void {
    // Explicit storePath overrides are already deterministic and should not auto-migrate.
    if (this.config.storePath) return;
    if (existsSync(this.storePath)) return;

    const legacyPath = getLegacyCronStorePath();
    if (legacyPath === this.storePath || !existsSync(legacyPath)) return;

    try {
      mkdirSync(dirname(this.storePath), { recursive: true });
      copyFileSync(legacyPath, this.storePath);
      logEvent('store_migrated', { from: legacyPath, to: this.storePath });
    } catch (e) {
      log.error('Failed to migrate legacy store:', e);
    }
  }

  /**
   * Multi-agent upgrade: if this agent has a per-agent storePath but the file
   * doesn't exist yet, copy from the global cron-jobs.json as a starting point.
   * Only the first agent to start gets the migration (others start empty).
   */
  private migrateFromGlobalStoreIfNeeded(): void {
    if (!this.config.storePath) return; // Not in multi-agent mode
    if (existsSync(this.storePath)) return; // Already has own store

    const globalPath = getCronStorePath();
    if (globalPath === this.storePath || !existsSync(globalPath)) return;

    try {
      mkdirSync(dirname(this.storePath), { recursive: true });
      copyFileSync(globalPath, this.storePath);
      // Rename global file so subsequent agents don't also copy it
      renameSync(globalPath, globalPath + '.migrated');
      logEvent('store_migrated_from_global', { from: globalPath, to: this.storePath });
    } catch (e) {
      log.error('Failed to migrate from global cron store:', e);
    }
  }
  
  private loadJobs(): void {
    try {
      if (existsSync(this.storePath)) {
        const data: CronStoreFile = JSON.parse(readFileSync(this.storePath, 'utf-8'));
        for (const job of data.jobs) {
          // Restore Date objects
          if (job.state.lastRunAt) {
            job.state.lastRunAt = new Date(job.state.lastRunAt);
          }
          if (job.state.nextRunAt) {
            job.state.nextRunAt = new Date(job.state.nextRunAt);
          }
          this.jobs.set(job.id, job);
        }
        log.info(`Loaded ${this.jobs.size} jobs`);
      }
    } catch (e) {
      log.error('Failed to load jobs:', e);
    }
  }
  
  private saveJobs(): void {
    try {
      const data: CronStoreFile = {
        version: 1,
        jobs: Array.from(this.jobs.values()),
      };
      // Ensure directory exists (important for Railway volumes)
      mkdirSync(dirname(this.storePath), { recursive: true });
      writeFileSync(this.storePath, JSON.stringify(data, null, 2));
    } catch (e) {
      log.error('Failed to save jobs:', e);
    }
  }
  
  private generateId(): string {
    return `cron-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
  }
  
  async start(): Promise<void> {
    if (this.started) return;
    
    // Dynamic import
    schedule = await import('node-schedule');
    
    // Schedule all enabled jobs
    for (const job of this.jobs.values()) {
      if (job.enabled) {
        this.scheduleJob(job);
      }
    }
    
    // Start heartbeat if configured
    const heartbeat = this.config.heartbeat || DEFAULT_HEARTBEAT;
    if (heartbeat.enabled) {
      this.startHeartbeat(heartbeat);
    }
    
    // Start file watcher for hot-reload
    this.startFileWatcher();
    
    this.started = true;
    const enabledCount = Array.from(this.jobs.values()).filter(j => j.enabled).length;
    log.info(`Service started (${enabledCount} jobs, heartbeat: ${heartbeat.enabled ? 'on' : 'off'}, watching for changes)`);
  }
  
  stop(): void {
    // Stop file watcher
    if (this.fileWatcher) {
      this.fileWatcher.close();
      this.fileWatcher = null;
    }
    
    // Cancel all scheduled jobs
    for (const scheduledJob of this.scheduledJobs.values()) {
      scheduledJob.cancel();
    }
    this.scheduledJobs.clear();
    
    // Cancel heartbeat
    if (this.heartbeatJob) {
      this.heartbeatJob.cancel();
      this.heartbeatJob = null;
    }
    
    this.started = false;
    log.info('Service stopped');
  }
  
  /**
   * Start watching the cron-jobs.json file for changes
   * This allows the agent to create jobs via CLI and have them scheduled immediately
   */
  private startFileWatcher(): void {
    // Store initial content for comparison
    try {
      if (existsSync(this.storePath)) {
        this.lastFileContent = readFileSync(this.storePath, 'utf-8');
      }
    } catch {
      // Ignore
    }
    
    // Watch for changes
    try {
      this.fileWatcher = watch(this.storePath, { persistent: false }, (eventType) => {
        if (eventType === 'change') {
          this.handleFileChange();
        }
      });
    } catch {
      // File might not exist yet, watch the directory instead
      const dir = resolve(this.storePath, '..');
      this.fileWatcher = watch(dir, { persistent: false }, (eventType, filename) => {
        if (filename === 'cron-jobs.json') {
          this.handleFileChange();
        }
      });
    }
  }
  
  /**
   * Handle changes to the cron-jobs.json file
   */
  private handleFileChange(): void {
    try {
      if (!existsSync(this.storePath)) return;
      
      const newContent = readFileSync(this.storePath, 'utf-8');
      
      // Skip if content hasn't actually changed (debounce)
      if (newContent === this.lastFileContent) return;
      this.lastFileContent = newContent;
      
      logEvent('file_changed', { path: this.storePath });
      
      // Reload jobs
      this.reloadJobs();
    } catch (e) {
      log.error('Error handling file change:', e);
    }
  }
  
  /**
   * Reload jobs from disk and reschedule
   */
  private reloadJobs(): void {
    // Cancel all existing scheduled jobs
    for (const scheduledJob of this.scheduledJobs.values()) {
      scheduledJob.cancel();
    }
    this.scheduledJobs.clear();
    this.jobs.clear();
    
    // Reload from disk
    this.loadJobs();
    
    // Reschedule all enabled jobs
    for (const job of this.jobs.values()) {
      if (job.enabled) {
        this.scheduleJob(job);
      }
    }
    
    const enabledCount = Array.from(this.jobs.values()).filter(j => j.enabled).length;
    logEvent('jobs_reloaded', { total: this.jobs.size, enabled: enabledCount });
  }
  
  /**
   * Start the heartbeat check-in (SILENT MODE - no auto-delivery)
   */
  private startHeartbeat(config: HeartbeatConfig): void {
    this.heartbeatJob = schedule.scheduleJob(config.schedule, async () => {
      logEvent('heartbeat_running', { schedule: config.schedule });
      
      try {
        // SILENT MODE - response NOT auto-delivered
        // Agent must use `lettabot-message` CLI to send messages
        const response = await this.bot.sendToAgent(config.message);
        
        log.info(`Heartbeat finished (SILENT MODE)`);
        log.info(`  - Response: ${response?.slice(0, 100)}${(response?.length || 0) > 100 ? '...' : ''}`);
        log.info(`  - (Response NOT auto-delivered - agent uses lettabot-message CLI)`);
      } catch (error) {
        log.error('Heartbeat failed:', error);
      }
    });
    
    const next = this.heartbeatJob.nextInvocation();
    logEvent('heartbeat_scheduled', {
      schedule: config.schedule,
      nextRun: next?.toISOString() || null,
    });
  }
  
  /**
   * Update heartbeat configuration
   */
  setHeartbeat(config: Partial<HeartbeatConfig>): void {
    const current = this.config.heartbeat || DEFAULT_HEARTBEAT;
    this.config.heartbeat = { ...current, ...config };
    
    // Restart heartbeat if running
    if (this.started) {
      if (this.heartbeatJob) {
        this.heartbeatJob.cancel();
        this.heartbeatJob = null;
      }
      
      if (this.config.heartbeat.enabled) {
        this.startHeartbeat(this.config.heartbeat);
      }
    }
  }
  
  /**
   * Get heartbeat configuration
   */
  getHeartbeat(): HeartbeatConfig {
    return this.config.heartbeat || DEFAULT_HEARTBEAT;
  }
  
  private scheduleJob(job: CronJob): void {
    const rule = this.parseSchedule(job.schedule);
    if (!rule) {
      log.warn(`Invalid schedule for job ${job.name}`);
      return;
    }
    
    const scheduledJob = schedule.scheduleJob(rule, async () => {
      await this.runJob(job.id);
    });
    
    if (scheduledJob) {
      this.scheduledJobs.set(job.id, scheduledJob);
      
      // Update next run time
      const nextInvocation = scheduledJob.nextInvocation();
      if (nextInvocation) {
        job.state.nextRunAt = new Date(nextInvocation.getTime());
        this.saveJobs();
        
        log.info(`📅 Scheduled "${job.name}" - next run: ${job.state.nextRunAt.toLocaleString()}`);
        
        logEvent('job_scheduled', {
          id: job.id,
          name: job.name,
          schedule: job.schedule.kind === 'cron' ? job.schedule.expr : job.schedule.kind,
          nextRun: job.state.nextRunAt.toISOString(),
        });
      }
    }
  }
  
  private parseSchedule(sched: CronSchedule): string | Date | import('node-schedule').RecurrenceRule | null {
    switch (sched.kind) {
      case 'cron':
        return sched.expr;
      case 'at':
        return new Date(sched.date);
      case 'every': {
        // For intervals, use RecurrenceRule
        const rule = new schedule.RecurrenceRule();
        // Convert ms to appropriate interval
        const seconds = Math.floor(sched.ms / 1000);
        if (seconds < 60) {
          rule.second = new schedule.Range(0, 59, seconds);
        } else {
          const minutes = Math.floor(seconds / 60);
          rule.minute = new schedule.Range(0, 59, minutes);
        }
        return rule;
      }
      default:
        return null;
    }
  }
  
  private async runJob(jobId: string): Promise<void> {
    const job = this.jobs.get(jobId);
    if (!job) return;
    
    log.info(`${'='.repeat(50)}`);
    const isEmailCheck = job.name.toLowerCase().includes('email') || job.message.includes('gog gmail');
    const icon = isEmailCheck ? '📧' : '⏰';
    log.info(`${icon} RUNNING JOB: ${job.name}`);
    log.info(`       ID: ${job.id}`);
    if (isEmailCheck) {
      log.info(`       Checking Gmail for new messages...`);
    } else {
      log.info(`       Message: ${job.message.slice(0, 100)}${job.message.length > 100 ? '...' : ''}`);
    }
    log.info(`${'='.repeat(50)}`);
    
    logEvent('job_running', { id: job.id, name: job.name });
    
    try {
      // Format message with metadata
      const now = new Date();
      const formattedTime = now.toLocaleString();
      const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
      const messageWithMetadata = [
        `[cron:${job.id} ${job.name}] ${job.message}`,
        `Current time: ${formattedTime} (${timezone})`,
      ].join('\n');
      
      // Send message to agent
      const response = await this.bot.sendToAgent(messageWithMetadata);
      
      // Resolve delivery target: explicit config > last message target fallback > silent
      let deliverTarget: { channel: string; chatId: string } | null = job.deliver ?? null;
      if (!deliverTarget && !job.silent) {
        const last = this.bot.getLastMessageTarget();
        if (last) {
          log.info(`No deliver target configured for "${job.name}", using last message target: ${last.channel}:${last.chatId}`);
          deliverTarget = { channel: last.channel, chatId: last.chatId };
        }
      }
      const deliverMode = deliverTarget ? 'deliver' : 'silent';
      if (deliverTarget && response) {
        try {
          await this.bot.deliverToChannel(deliverTarget.channel, deliverTarget.chatId, { text: response });
          log.info(`📬 Delivered response to ${deliverTarget.channel}:${deliverTarget.chatId}`);
        } catch (deliverError) {
          log.error(`Failed to deliver response to ${deliverTarget.channel}:${deliverTarget.chatId}:`, deliverError);
          logEvent('job_deliver_failed', {
            id: job.id,
            name: job.name,
            channel: deliverTarget.channel,
            chatId: deliverTarget.chatId,
            error: deliverError instanceof Error ? deliverError.message : String(deliverError),
          });
        }
      }
      
      // Update state
      job.state.lastRunAt = new Date();
      job.state.lastStatus = 'ok';
      job.state.lastError = undefined;
      job.state.lastResponse = response?.slice(0, 500); // Store truncated response
      
      // Update next run time
      const scheduled = this.scheduledJobs.get(jobId);
      if (scheduled) {
        const next = scheduled.nextInvocation();
        if (next) {
          job.state.nextRunAt = new Date(next.getTime());
        }
      }
      
      log.info(`${'='.repeat(50)}`);
      log.info(`✅ JOB COMPLETED: ${job.name} [${deliverMode.toUpperCase()} MODE]`);
      log.info(`       Response: ${response?.slice(0, 200)}${(response?.length || 0) > 200 ? '...' : ''}`);
      if (deliverMode === 'silent') {
        log.info(`       (Response NOT auto-delivered - no target available)`);
      } else {
        log.info(`       (Response delivered to ${deliverTarget!.channel}:${deliverTarget!.chatId}${!job.deliver ? ' via fallback' : ''})`);
      }
      log.info(`${'='.repeat(50)}`);
      
      logEvent('job_completed', {
        id: job.id,
        name: job.name,
        status: 'ok',
        mode: deliverMode,
        deliverTarget: deliverTarget ? `${deliverTarget.channel}:${deliverTarget.chatId}` : undefined,
        deliverSource: job.deliver ? 'configured' : (deliverTarget ? 'lastMessageTarget' : undefined),
        nextRun: job.state.nextRunAt?.toISOString(),
        responseLength: response?.length || 0,
      });
      
      // Delete if one-shot
      if (job.deleteAfterRun) {
        this.remove(jobId);
      } else {
        this.saveJobs();
      }
      
    } catch (error) {
      logEvent('job_failed', {
        id: job.id,
        name: job.name,
        error: error instanceof Error ? error.message : String(error),
      });
      job.state.lastRunAt = new Date();
      job.state.lastStatus = 'error';
      job.state.lastError = error instanceof Error ? error.message : String(error);
      this.saveJobs();
    }
  }
  
  // Public API
  
  add(input: CronJobCreate): CronJob {
    const job: CronJob = {
      ...input,
      id: this.generateId(),
      state: {},
    };
    
    this.jobs.set(job.id, job);
    this.saveJobs();
    
    if (this.started && job.enabled) {
      this.scheduleJob(job);
    }
    
    log.info(`Added job: ${job.name}`);
    return job;
  }
  
  remove(jobId: string): boolean {
    const scheduledJob = this.scheduledJobs.get(jobId);
    if (scheduledJob) {
      scheduledJob.cancel();
      this.scheduledJobs.delete(jobId);
    }
    
    const deleted = this.jobs.delete(jobId);
    if (deleted) {
      this.saveJobs();
      log.info(`Removed job: ${jobId}`);
    }
    return deleted;
  }
  
  enable(jobId: string): void {
    const job = this.jobs.get(jobId);
    if (!job) return;
    
    job.enabled = true;
    this.saveJobs();
    
    if (this.started) {
      this.scheduleJob(job);
    }
  }
  
  disable(jobId: string): void {
    const job = this.jobs.get(jobId);
    if (!job) return;
    
    job.enabled = false;
    this.saveJobs();
    
    const scheduledJob = this.scheduledJobs.get(jobId);
    if (scheduledJob) {
      scheduledJob.cancel();
      this.scheduledJobs.delete(jobId);
    }
  }
  
  list(): CronJob[] {
    return Array.from(this.jobs.values());
  }
  
  get(jobId: string): CronJob | undefined {
    return this.jobs.get(jobId);
  }
  
  /**
   * Run a job immediately (for testing)
   */
  async runNow(jobId: string): Promise<void> {
    await this.runJob(jobId);
  }
  
  /**
   * Get service status
   */
  getStatus(): {
    running: boolean;
    totalJobs: number;
    enabledJobs: number;
    heartbeat: HeartbeatConfig;
  } {
    const jobs = Array.from(this.jobs.values());
    return {
      running: this.started,
      totalJobs: jobs.length,
      enabledJobs: jobs.filter(j => j.enabled).length,
      heartbeat: this.getHeartbeat(),
    };
  }
}
