/**
 * Compute Job management for Synor Compute SDK
 */

import type { JobResult, JobStatus, JobMetrics } from './types';
import type { SynorCompute } from './client';

const POLL_INTERVAL_MS = 500;
const MAX_POLL_ATTEMPTS = 120; // 60 seconds max

/**
 * Represents a compute job on the Synor network.
 */
export class ComputeJob<T = unknown> {
  readonly jobId: string;
  private client: SynorCompute;
  private status: JobStatus = 'pending';
  private result?: T;
  private error?: string;
  private metrics?: JobMetrics;
  private callbacks: Map<JobStatus, ((job: ComputeJob<T>) => void)[]> = new Map();

  constructor(jobId: string, client: SynorCompute) {
    this.jobId = jobId;
    this.client = client;
  }

  /**
   * Get current job status.
   */
  getStatus(): JobStatus {
    return this.status;
  }

  /**
   * Check if job is complete (success or failure).
   */
  isComplete(): boolean {
    return this.status === 'completed' || this.status === 'failed' || this.status === 'cancelled';
  }

  /**
   * Wait for job to complete.
   */
  async wait(options?: { timeout?: number; pollInterval?: number }): Promise<JobResult<T>> {
    const timeout = options?.timeout ?? MAX_POLL_ATTEMPTS * POLL_INTERVAL_MS;
    const pollInterval = options?.pollInterval ?? POLL_INTERVAL_MS;
    const maxAttempts = Math.ceil(timeout / pollInterval);

    let attempts = 0;
    while (!this.isComplete() && attempts < maxAttempts) {
      await this.refresh();
      if (!this.isComplete()) {
        await sleep(pollInterval);
      }
      attempts++;
    }

    if (!this.isComplete()) {
      throw new JobTimeoutError(this.jobId, timeout);
    }

    return {
      jobId: this.jobId,
      status: this.status,
      data: this.result,
      error: this.error,
      metrics: this.metrics,
    };
  }

  /**
   * Refresh job status from server.
   */
  async refresh(): Promise<void> {
    const response = await this.client.getJobStatus(this.jobId);
    const previousStatus = this.status;

    this.status = response.status;
    this.result = response.data as T;
    this.error = response.error;
    this.metrics = response.metrics;

    // Trigger callbacks if status changed
    if (previousStatus !== this.status) {
      this.triggerCallbacks(this.status);
    }
  }

  /**
   * Cancel the job.
   */
  async cancel(): Promise<void> {
    await this.client.cancelJob(this.jobId);
    this.status = 'cancelled';
    this.triggerCallbacks('cancelled');
  }

  /**
   * Register a callback for status changes.
   */
  on(status: JobStatus, callback: (job: ComputeJob<T>) => void): this {
    const callbacks = this.callbacks.get(status) ?? [];
    callbacks.push(callback);
    this.callbacks.set(status, callbacks);
    return this;
  }

  /**
   * Get the result (throws if not complete or failed).
   */
  getResult(): T {
    if (this.status !== 'completed') {
      throw new Error(`Job ${this.jobId} is not completed (status: ${this.status})`);
    }
    if (this.result === undefined) {
      throw new Error(`Job ${this.jobId} has no result`);
    }
    return this.result;
  }

  /**
   * Get error message (if failed).
   */
  getError(): string | undefined {
    return this.error;
  }

  /**
   * Get execution metrics (if available).
   */
  getMetrics(): JobMetrics | undefined {
    return this.metrics;
  }

  /**
   * Convert to JSON-serializable object.
   */
  toJSON(): JobResult<T> {
    return {
      jobId: this.jobId,
      status: this.status,
      data: this.result,
      error: this.error,
      metrics: this.metrics,
    };
  }

  private triggerCallbacks(status: JobStatus): void {
    const callbacks = this.callbacks.get(status) ?? [];
    for (const callback of callbacks) {
      try {
        callback(this);
      } catch (err) {
        console.error(`Error in job callback for status ${status}:`, err);
      }
    }
  }
}

/**
 * Error thrown when a job times out.
 */
export class JobTimeoutError extends Error {
  constructor(
    public jobId: string,
    public timeout: number
  ) {
    super(`Job ${jobId} timed out after ${timeout}ms`);
    this.name = 'JobTimeoutError';
  }
}

/**
 * Error thrown when a job fails.
 */
export class JobFailedError extends Error {
  constructor(
    public jobId: string,
    public reason: string
  ) {
    super(`Job ${jobId} failed: ${reason}`);
    this.name = 'JobFailedError';
  }
}

/**
 * Utility to sleep for a given duration.
 */
function sleep(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}

/**
 * Batch multiple jobs together for efficient execution.
 */
export class JobBatch<T = unknown> {
  private jobs: ComputeJob<T>[] = [];

  /**
   * Add a job to the batch.
   */
  add(job: ComputeJob<T>): this {
    this.jobs.push(job);
    return this;
  }

  /**
   * Get all jobs in the batch.
   */
  getJobs(): ComputeJob<T>[] {
    return [...this.jobs];
  }

  /**
   * Wait for all jobs to complete.
   */
  async waitAll(options?: { timeout?: number }): Promise<JobResult<T>[]> {
    return Promise.all(this.jobs.map(job => job.wait(options)));
  }

  /**
   * Wait for any job to complete.
   */
  async waitAny(options?: { timeout?: number }): Promise<JobResult<T>> {
    return Promise.race(this.jobs.map(job => job.wait(options)));
  }

  /**
   * Cancel all jobs.
   */
  async cancelAll(): Promise<void> {
    await Promise.all(this.jobs.map(job => job.cancel()));
  }

  /**
   * Get count of jobs by status.
   */
  getStatusCounts(): Record<JobStatus, number> {
    const counts: Record<JobStatus, number> = {
      pending: 0,
      queued: 0,
      running: 0,
      completed: 0,
      failed: 0,
      cancelled: 0,
    };

    for (const job of this.jobs) {
      counts[job.getStatus()]++;
    }

    return counts;
  }
}
