// ABOUTME: Abstract base class for all scheduled jobs with shared functionality
// ABOUTME: Provides cron execution, logging, metrics, error handling, and tracing

import cron from 'node-cron';
import { trace, SpanStatusCode } from '@opentelemetry/api';
import { Counter, Histogram, Gauge } from 'prom-client';
import { PrismaClient } from '@prisma/client';
import logger from '../utils/logger';
import { ScheduleType, SchedulerConfig, JobContext, JobResult } from './types';

// ============================================================================
// Metrics
// ============================================================================

const tracer = trace.getTracer('scheduler');

const schedulerRunsCounter = new Counter({
  name: 'scheduler_runs_total',
  help: 'Total number of scheduler executions',
  labelNames: ['schedule_type', 'status'],
});

const schedulerDuration = new Histogram({
  name: 'scheduler_duration_seconds',
  help: 'Duration of scheduler executions',
  labelNames: ['schedule_type'],
  buckets: [1, 5, 10, 30, 60, 120, 300],
});

const schedulerItemsProcessed = new Counter({
  name: 'scheduler_items_processed_total',
  help: 'Total items processed by schedulers',
  labelNames: ['schedule_type'],
});

const schedulerLastSuccessTimestamp = new Gauge({
  name: 'scheduler_last_success_timestamp_seconds',
  help: 'Unix timestamp of last successful scheduler execution',
  labelNames: ['schedule_type'],
});

const schedulerCurrentlyRunning = new Gauge({
  name: 'scheduler_currently_running',
  help: 'Whether scheduler is currently executing (1 = running, 0 = idle)',
  labelNames: ['schedule_type'],
});

const schedulerErrorsCounter = new Counter({
  name: 'scheduler_errors_total',
  help: 'Total errors by scheduler and error type',
  labelNames: ['schedule_type', 'error_type'],
});

const schedulerItemsFailed = new Counter({
  name: 'scheduler_items_failed_total',
  help: 'Total items that failed processing',
  labelNames: ['schedule_type'],
});

// ============================================================================
// BaseScheduler Class
// ============================================================================

export abstract class BaseScheduler {
  protected task: cron.ScheduledTask | null = null;
  protected isRunning = false;

  constructor(
    protected readonly type: ScheduleType,
    protected readonly config: SchedulerConfig,
    protected readonly prisma: PrismaClient
  ) {
    logger.info({
      type,
      cronExpression: config.cronExpression,
      timezone: config.timezone,
      enabled: config.enabled,
    }, `Scheduler initialized: ${type}`);
  }

  /**
   * Start the scheduler
   */
  start(): void {
    if (!this.config.enabled) {
      logger.info({ type: this.type }, 'Scheduler is disabled, not starting');
      return;
    }

    if (this.task) {
      logger.warn({ type: this.type }, 'Scheduler already running');
      return;
    }

    this.task = cron.schedule(
      this.config.cronExpression,
      async () => {
        await this.execute();
      },
      {
        timezone: this.config.timezone || 'Pacific/Auckland',
      }
    );

    logger.info({
      type: this.type,
      cronExpression: this.config.cronExpression,
    }, 'Scheduler started');
  }

  /**
   * Stop the scheduler
   */
  stop(): void {
    if (!this.task) {
      logger.warn({ type: this.type }, 'Scheduler not running');
      return;
    }

    this.task.stop();
    this.task = null;

    logger.info({ type: this.type }, 'Scheduler stopped');
  }

  /**
   * Execute the scheduled job
   */
  private async execute(retryCount = 0, parentJobRunId?: string): Promise<void> {
    if (this.isRunning && retryCount === 0) {
      logger.warn({ type: this.type }, 'Previous execution still running, skipping');
      return;
    }

    const span = tracer.startSpan(`scheduler.${this.type}`);
    const startTime = Date.now();
    this.isRunning = true;

    // Set gauge to indicate scheduler is running
    schedulerCurrentlyRunning.labels(this.type).set(1);

    // Create JobRun record
    const jobRun = await this.prisma.jobRun.create({
      data: {
        jobType: this.type,
        status: 'running',
        retryCount,
        parentJobRunId,
      },
    });

    try {
      span.setAttributes({
        'scheduler.type': this.type,
        'scheduler.triggered_at': new Date().toISOString(),
        'scheduler.job_run_id': jobRun.id,
        'scheduler.retry_count': retryCount,
      });

      logger.info({
        type: this.type,
        jobRunId: jobRun.id,
        retryCount,
      }, 'Scheduler execution started');

      const context: JobContext = {
        type: this.type,
        triggeredAt: new Date(),
        jobRunId: jobRun.id,
        data: {
          retryCount,
          parentJobRunId,
        },
      };

      // Execute the job implementation
      const result = await this.executeJob(context);

      // Calculate duration
      const durationMs = Date.now() - startTime;
      const durationSeconds = durationMs / 1000;

      // Determine final status
      const status = result.success
        ? 'success'
        : (result.itemsProcessed > 0 ? 'partial' : 'failed');

      // Update JobRun record
      await this.prisma.jobRun.update({
        where: { id: jobRun.id },
        data: {
          status,
          completedAt: new Date(),
          durationMs,
          itemsProcessed: result.itemsProcessed,
          itemsFailed: result.errors.length,
          errorMessage: result.errors.length > 0 ? result.errors.join('; ') : null,
          metadata: result.metadata,
        },
      });

      // Record metrics
      schedulerDuration.labels(this.type).observe(durationSeconds);
      schedulerItemsProcessed.labels(this.type).inc(result.itemsProcessed);
      schedulerItemsFailed.labels(this.type).inc(result.errors.length);

      if (result.success) {
        schedulerRunsCounter.labels(this.type, 'success').inc();
        schedulerLastSuccessTimestamp.labels(this.type).set(Date.now() / 1000);
        span.setStatus({ code: SpanStatusCode.OK });

        logger.info({
          type: this.type,
          jobRunId: jobRun.id,
          itemsProcessed: result.itemsProcessed,
          durationMs,
          errors: result.errors.length,
        }, 'Scheduler execution completed successfully');
      } else {
        schedulerRunsCounter.labels(this.type, status).inc();
        schedulerErrorsCounter.labels(this.type, 'job_failure').inc();
        span.setStatus({ code: SpanStatusCode.ERROR, message: `Completed with status: ${status}` });

        logger.warn({
          type: this.type,
          jobRunId: jobRun.id,
          itemsProcessed: result.itemsProcessed,
          durationMs,
          errors: result.errors.length,
        }, 'Scheduler execution completed with errors');
      }

      span.setAttributes({
        'scheduler.items_processed': result.itemsProcessed,
        'scheduler.errors': result.errors.length,
        'scheduler.duration_ms': durationMs,
        'scheduler.status': status,
      });

    } catch (error) {
      const durationMs = Date.now() - startTime;
      const errorMessage = error instanceof Error ? error.message : 'Unknown error';

      // Update JobRun with failure
      await this.prisma.jobRun.update({
        where: { id: jobRun.id },
        data: {
          status: 'failed',
          completedAt: new Date(),
          durationMs,
          errorMessage,
        },
      });

      schedulerRunsCounter.labels(this.type, 'failure').inc();
      schedulerDuration.labels(this.type).observe(durationMs / 1000);
      schedulerErrorsCounter.labels(this.type, 'exception').inc();

      span.setStatus({
        code: SpanStatusCode.ERROR,
        message: errorMessage,
      });

      logger.error({
        type: this.type,
        jobRunId: jobRun.id,
        error: errorMessage,
        stack: error instanceof Error ? error.stack : undefined,
        durationMs,
      }, 'Scheduler execution failed');

      // Don't throw - let the next scheduled run proceed
    } finally {
      this.isRunning = false;
      schedulerCurrentlyRunning.labels(this.type).set(0);
      span.end();
    }
  }

  /**
   * Abstract method - implement the actual job logic
   */
  protected abstract executeJob(context: JobContext): Promise<JobResult>;

  /**
   * Schedule a retry for this job
   * @param delayMs Delay in milliseconds before retry
   * @param parentJobRunId ID of the parent job that failed
   * @param retryCount Current retry attempt number
   */
  protected scheduleRetry(delayMs: number, parentJobRunId: string, retryCount: number): void {
    logger.info({
      type: this.type,
      delayMs,
      parentJobRunId,
      retryCount,
    }, 'Scheduling retry');

    setTimeout(async () => {
      await this.execute(retryCount, parentJobRunId);
    }, delayMs);
  }

  /**
   * Check if scheduler is currently running
   */
  isCurrentlyRunning(): boolean {
    return this.isRunning;
  }

  /**
   * Get scheduler status
   */
  getStatus() {
    return {
      type: this.type,
      enabled: this.config.enabled,
      running: this.task !== null,
      currentlyExecuting: this.isRunning,
      cronExpression: this.config.cronExpression,
      description: this.config.description,
    };
  }
}
