/**
 * Recurring Expenses Background Scheduler Service
 * 
 * This service runs as a background worker and:
 * 1. Checks active schedule jobs every N seconds
 * 2. Generates expense documents for due dates
 * 3. Creates obligations and postings automatically
 * 4. Applies escalation rules
 * 5. Tracks all operations in job logs
 * 
 * Can be run as:
 * - npm run scheduler:start
 * - node --experimental-worker lib/scheduler-service.js
 * - Cron job: node scripts/scheduler.js (every 5 minutes)
 */

import type {
  RecurringScheduleJob,
  RecurringExpenseTemplate,
  RecurringExpenseFrequency,
  ExpenseObligation,
  ExpensePosting,
  PostingType,
} from '@/lib/types';

import {
  pgListActiveScheduleJobs,
  pgCreateBackgroundJobLog,
  pgUpdateBackgroundJobLog,
  pgCheckGenerationExists,
  pgCreateGenerationDuplicate,
  pgUpdateScheduleJob,
  pgUpdateSchedulerStatus,
  pgCreateEscalationExecution,
} from '@/lib/postgres/scheduler';

import {
  pgGetRecurringContractById,
  pgCreateExpenseObligation,
  pgCreateExpensePosting,
} from '@/lib/postgres/expense-advanced';

import { pgGetRecurringExpenseTemplateById } from '@/lib/postgres/expense-recurring';

import { pgQuery } from '@/lib/postgres/client';

export class RecurringExpenseSchedulerService {
  private isRunning = false;
  private checkInterval = 300000; // 5 minutes in milliseconds
  private jobTimeout = 3600000; // 1 hour
  private maxConcurrentJobs = 5;

  constructor() {
    console.log('[Scheduler] Service initialized');
    this.setupGracefulShutdown();
  }

  /**
   * Start the scheduler service
   */
  public async start(): Promise<void> {
    if (this.isRunning) {
      console.warn('[Scheduler] Service is already running');
      return;
    }

    this.isRunning = true;
    console.log('[Scheduler] Service started');

    // Initial run
    await this.runScheduleCycle();

    // Schedule recurring checks
    setInterval(() => {
      if (this.isRunning) {
        this.runScheduleCycle().catch(err => {
          console.error('[Scheduler] Error in cycle:', err);
        });
      }
    }, this.checkInterval);
  }

  /**
   * Stop the scheduler service
   */
  public async stop(): Promise<void> {
    this.isRunning = false;
    console.log('[Scheduler] Service stopped');
  }

  /**
   * Main scheduling cycle
   */
  private async runScheduleCycle(): Promise<void> {
    const startTime = Date.now();
    console.log(`[Scheduler] Running cycle at ${new Date().toISOString()}`);

    try {
      // 1. Update scheduler status - running
      await pgUpdateSchedulerStatus({
        isRunning: true,
        lastCheckAt: new Date().toISOString(),
      });

      // 2. Get active schedule jobs that are due
      const activeJobs = await pgListActiveScheduleJobs();

      if (activeJobs.length === 0) {
        console.log('[Scheduler] No active jobs due at this time');
        return;
      }

      console.log(`[Scheduler] Found ${activeJobs.length} active jobs`);

      // 3. Process jobs with concurrency limit
      const batchSize = this.maxConcurrentJobs;
      for (let i = 0; i < activeJobs.length; i += batchSize) {
        const batch = activeJobs.slice(i, i + batchSize);
        await Promise.all(batch.map(job => this.processScheduleJob(job)));
      }

      // 4. Update scheduler health status
      const elapsedTime = Date.now() - startTime;
      await pgUpdateSchedulerStatus({
        lastRunAt: new Date().toISOString(),
        averageJobDurationMs: Math.round(elapsedTime / Math.max(activeJobs.length, 1)),
        isHealthy: elapsedTime < this.jobTimeout,
      });

      console.log(`[Scheduler] Cycle completed in ${elapsedTime}ms`);
    } catch (error) {
      console.error('[Scheduler] Cycle failed:', error);
      await pgUpdateSchedulerStatus({
        isHealthy: false,
        lastErrorMessage: error instanceof Error ? error.message : 'Unknown error',
      }).catch(err => console.error('[Scheduler] Failed to update status:', err));
    }
  }

  /**
   * Process a single schedule job
   */
  private async processScheduleJob(job: RecurringScheduleJob): Promise<void> {
    console.log(`[Job ${job.id}] Starting processing`);
    let jobLogId: string | null = null;

    try {
      // 1. Create job log
      const jobLog = await pgCreateBackgroundJobLog({
        jobId: job.id,
        jobType: 'generate_document',
        status: 'running',
        createdBy: 'system',
      });
      if (!jobLog) {
        throw new Error(`Failed to create job log for job ${job.id}`);
      }
      jobLogId = jobLog.id;

      // 2. Get the template
      const template = await pgGetRecurringExpenseTemplateById(job.templateId);
      if (!template) {
        throw new Error(`Template ${job.templateId} not found`);
      }

      // 3. Get the contract for renewal/escalation info
      if (!job.contractId) {
        throw new Error(`Job ${job.id} has no contractId`);
      }
      const contract = await pgGetRecurringContractById(job.contractId);
      if (!contract) {
        throw new Error(`Contract ${job.contractId} not found`);
      }

      // 4. Calculate period dates
      const nextRunDate = new Date(job.nextRunDate);
      const periodStart = new Date(nextRunDate);
      const periodEnd = new Date(nextRunDate);

      switch (template.frequency || 'monthly') {
        case 'monthly':
          periodEnd.setMonth(periodEnd.getMonth() + 1);
          break;
        case 'yearly':
          periodEnd.setFullYear(periodEnd.getFullYear() + 1);
          break;
      }

      // 5. Check for duplicates
      const isDuplicate = await pgCheckGenerationExists(
        job.templateId,
        periodStart.toISOString().split('T')[0],
        periodEnd.toISOString().split('T')[0]
      );
      if (isDuplicate) {
        console.log(`[Job ${job.id}] Document already generated for this period`);
        // Update job log and return
        await pgUpdateBackgroundJobLog(jobLogId, {
          status: 'retry',
          completedAt: true,
          errorMessage: 'Duplicate period',
        });
        return;
      }

      // 6. Generate expense document
      const doc = await this.generateExpenseDocument(job, template, contract);
      console.log(`[Job ${job.id}] Document generated: ${doc.id}`);

      // 7. Record duplicate prevention
      await pgCreateGenerationDuplicate({
        jobId: job.id,
        templateId: job.templateId,
        periodStart: periodStart.toISOString().split('T')[0],
        periodEnd: periodEnd.toISOString().split('T')[0],
        generatedDocumentId: doc.id,
      });

      // 8. Create obligation if enabled
      if (job.autoCreateObligation) {
        const obligation = await this.createObligation(doc, job, template, contract);
        console.log(`[Job ${job.id}] Obligation created: ${obligation?.id}`);

        // 9. Create postings if enabled
        if (job.autoCreatePosting && obligation) {
          const postings = await this.createPostings(obligation, template);
          console.log(`[Job ${job.id}] Postings created: ${postings.length}`);
        }
      }

      // 10. Calculate next run date
      const nextRunDateCalc = this.calculateNextRunDate(job);
      await pgUpdateScheduleJob(job.id, {
        nextRunDate: nextRunDateCalc.toISOString(),
        lastRunDate: new Date().toISOString(),
        totalRuns: (job.totalRuns || 0) + 1,
        retryCount: 0,
        updatedBy: 'system',
      });

      // 11. Update job log to success
      await pgUpdateBackgroundJobLog(jobLogId, {
        status: 'success',
        completedAt: true,
        generatedDocuments: 1,
      });

      console.log(`[Job ${job.id}] Completed successfully`);
    } catch (error) {
      console.error(`[Job ${job.id}] Failed:`, error);

      // 1. Increment retry count
      const newRetryCount = (job.retryCount || 0) + 1;
      const shouldRetry = newRetryCount < (job.maxRetries || 3);

      // 2. Calculate backoff time (exponential)
      let nextRunDate = new Date();
      if (shouldRetry) {
        const backoffMs = Math.pow(2, newRetryCount) * 60000; // 1min, 2min, 4min, etc.
        nextRunDate = new Date(Date.now() + backoffMs);
      }

      // 3. Update job with error
      await pgUpdateScheduleJob(job.id, {
        retryCount: newRetryCount,
        lastError: error instanceof Error ? error.message : 'Unknown error',
        nextRunDate: shouldRetry ? nextRunDate.toISOString() : job.nextRunDate,
        status: shouldRetry ? 'active' : 'cancelled',
        updatedBy: 'system',
      });

      // 4. Update job log to failed
      if (jobLogId) {
        await pgUpdateBackgroundJobLog(jobLogId, {
          status: shouldRetry ? 'retry' : 'failed',
          completedAt: true,
          errorMessage: error instanceof Error ? error.message : 'Unknown error',
        });
      }

      if (!shouldRetry) {
        console.error(`[Job ${job.id}] Max retries exceeded`);
      }
    }
  }

  /**
   * Generate an expense document from a schedule job
   */
  private async generateExpenseDocument(
    job: RecurringScheduleJob,
    template: RecurringExpenseTemplate,
    contract: { id: string; startDate: string; escalationPolicy: string; escalationValue?: number }
  ): Promise<any> {
    console.log(`[Generator] Creating document for template ${template.id}`);

    // Calculate amount with escalation if needed
    let documentAmount = template.defaultAmount || 0;

    // Check if this is a renewal date and apply escalation
    const nextRunDate = new Date(job.nextRunDate);
    const contractStart = new Date(contract.startDate);

    // Check if we're in a new contract year and escalation is enabled
    if (contract.escalationPolicy === 'fixed_percent' && contract.escalationValue) {
      const yearsElapsed = Math.floor((nextRunDate.getTime() - contractStart.getTime()) / (365 * 24 * 60 * 60 * 1000));
      if (yearsElapsed > 0) {
        documentAmount = template.defaultAmount * Math.pow(1 + contract.escalationValue / 100, yearsElapsed);

        // Log escalation execution
        await pgCreateEscalationExecution({
          templateId: template.id,
          contractId: contract.id,
          executionDate: nextRunDate.toISOString(),
          previousAmount: template.defaultAmount,
          newAmount: documentAmount,
          escalationType: 'fixed_percent',
          increasePercent: contract.escalationValue,
          createdBy: 'system',
        });

        console.log(`[Generator] Applied escalation: ${template.defaultAmount} -> ${documentAmount}`);
      }
    }

    // Ensure unique constraint: (template_id, period_year, period_month)
    // Check if document already exists for this month
    const existingCheck = await pgQuery(
      `
      SELECT * FROM expense_documents 
      WHERE template_id = $1 
      AND period_year = $2 
      AND period_month = $3
      LIMIT 1
      `,
      [template.id, nextRunDate.getFullYear(), nextRunDate.getMonth() + 1]
    );

    if (existingCheck.rows.length > 0) {
      console.log(`[Generator] Document already exists for ${template.id} in ${nextRunDate.getMonth() + 1}/${nextRunDate.getFullYear()}`);
      return existingCheck.rows[0];
    }

    // Create expense document using the schema from Migration 030
    const dueDate = new Date(nextRunDate);
    dueDate.setMonth(dueDate.getMonth() + 1);

    const result = await pgQuery(
      `
      INSERT INTO expense_documents (
        template_id, expense_item_id, expense_account_code, payable_account_code,
        period_year, period_month, due_date, description, amount,
        currency_code, payment_method, status, created_by
      ) VALUES (
        $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13
      ) RETURNING *
      `,
      [
        template.id,
        template.expenseItemId,
        template.expenseAccountCode,
        template.payableAccountCode || '2130',
        nextRunDate.getFullYear(),
        nextRunDate.getMonth() + 1,
        dueDate.toISOString().split('T')[0],
        `${template.name} - ${nextRunDate.toLocaleDateString('ar-SA')}`,
        documentAmount,
        template.currencyCode || 'USD',
        'bank', // default payment method
        'draft',
        'system',
      ]
    );

    const doc = result.rows[0];
    console.log(`[Generator] Document created with ID: ${doc.id}`);

    return doc;
  }

  /**
   * Create obligation for generated document
   */
  private async createObligation(
    doc: any,
    job: RecurringScheduleJob,
    template: RecurringExpenseTemplate,
    contract: { id: string }
  ): Promise<ExpenseObligation | null> {
    console.log(`[Obligation] Creating obligation for document ${doc.id}`);

    // Calculate period dates from document period_year and period_month
    const periodStart = new Date(doc.period_year, doc.period_month - 1, 1);
    const periodEnd = new Date(doc.period_year, doc.period_month, 0); // Last day of month

    const obligation = await pgCreateExpenseObligation({
      documentId: doc.id,
      contractId: contract.id,
      templateId: template.id,
      periodStart: periodStart.toISOString().split('T')[0],
      periodEnd: periodEnd.toISOString().split('T')[0],
      grossAmount: doc.amount,
      currencyCode: doc.currency_code || template.currencyCode || 'USD',
      basis: job.obligationBasis || 'accrual',
      recognitionDate: periodStart.toISOString().split('T')[0],
      settlementDate: doc.due_date,
      status: 'pending',
      createdBy: 'system',
    });

    console.log(`[Obligation] Obligation created with ID: ${obligation.id}`);
    return obligation;
  }

  /**
   * Create postings for obligation
   */
  private async createPostings(
    obligation: ExpenseObligation,
    template: RecurringExpenseTemplate
  ): Promise<ExpensePosting[]> {
    console.log(`[Posting] Creating postings for obligation ${obligation.id}`);

    const postings: ExpensePosting[] = [];

    const debitAccountCode = template.expenseAccountCode || '5120';
    const creditAccountCode = template.payableAccountCode || '2130';

    // Get the document to use as reference
    const docResult = await pgQuery(
      'SELECT * FROM expense_documents WHERE id = $1',
      [obligation.documentId]
    );
    const doc = docResult.rows[0];

    // Determine posting type based on obligation basis
    let postingType: PostingType = 'regular';
    if (obligation.basis === 'prepaid') {
      postingType = 'adjustment';
    }

    const postingDate = new Date().toISOString().split('T')[0];

    // The posting record stores both debit and credit accounts in one row.
    const posting = await pgCreateExpensePosting({
      obligationId: obligation.id,
      documentId: obligation.documentId,
      postingDate,
      debitAccount: debitAccountCode,
      creditAccount: creditAccountCode,
      amount: obligation.grossAmount,
      postingType,
      status: 'pending',
      createdBy: 'system',
    });

    postings.push(posting);
    console.log(`[Posting] Posting created: ${posting.id}`);

    return postings;
  }

  /**
   * Calculate next run date based on schedule
   */
  private calculateNextRunDate(job: RecurringScheduleJob): Date {
    const current = new Date(job.nextRunDate);

    // Get frequency from template (would be fetched from DB in production)
    // For now, default to monthly (supported frequencies: monthly, yearly)
    const frequency: RecurringExpenseFrequency =
      (job.raw?.frequency as RecurringExpenseFrequency) === 'yearly' ? 'yearly' : 'monthly';

    const next = new Date(current);

    switch (frequency) {
      case 'monthly':
        next.setMonth(next.getMonth() + 1);
        break;
      case 'yearly':
        next.setFullYear(next.getFullYear() + 1);
        break;
      default:
        next.setMonth(next.getMonth() + 1); // Default to monthly
    }

    // Handle weekends (Friday=4, Saturday=5 in most Middle East countries)
    if (job.skipWeekends && this.isWeekend(next)) {
      // Move to next Sunday
      const dayDiff = next.getDay() === 4 ? 3 : 2;
      next.setDate(next.getDate() + dayDiff);
    }

    // TODO: Handle holidays from holiday calendar table

    return next;
  }

  /**
   * Check if date is weekend (Friday or Saturday in most Middle East countries)
   */
  private isWeekend(date: Date): boolean {
    const day = date.getDay();
    return day === 4 || day === 5; // Friday and Saturday
  }

  /**
   * Setup graceful shutdown
   */
  private setupGracefulShutdown(): void {
    process.on('SIGTERM', () => {
      console.log('[Scheduler] SIGTERM received, shutting down gracefully...');
      this.stop();
      process.exit(0);
    });

    process.on('SIGINT', () => {
      console.log('[Scheduler] SIGINT received, shutting down gracefully...');
      this.stop();
      process.exit(0);
    });
  }
}

/**
 * CLI Entry Point
 */
if (require.main === module) {
  const scheduler = new RecurringExpenseSchedulerService();

  scheduler.start().catch(err => {
    console.error('Failed to start scheduler:', err);
    process.exit(1);
  });

  console.log('Recurring Expense Scheduler is running. Press Ctrl+C to stop.');
}

export default RecurringExpenseSchedulerService;
