import type { Logger } from "pino";
import { QUEUE, OBSERVABILITY, SHUTDOWN } from "@/core/constants.js";
import { JobTimeoutError, JobAbortedError } from "@/core/errors.js";
import type { JobPayload, IJobQueue, JobHandler, JobOutcome, JobKind } from "@/types/index.js";
import { METRIC } from "@/metrics/names.js";
import type { Metrics } from "@/metrics/types.js";
import { NoopMetrics } from "@/metrics/noop-recorder.js";

function jobLogKey(job: JobPayload): string {
  if (job.jobKind === "jira") return job.issueKey;
  if (job.jobKind === "pr_comment") return `${job.repoName}#${job.prNumber}`;
  if (job.jobKind === "pr_mention")
    return `mention:${job.repoName}#${job.prNumber}#${job.commentId}`;
  if (job.jobKind === "jira_ticket_review") {
    return `review:${job.issueKey}:${job.triggeredByCommentId}`;
  }
  return `slack:${job.channelId}:${job.messageTs}`;
}

const HIGH_LOAD_COOLDOWN_MS = 5 * 60 * 1000;

export interface QueueHighLoadState {
  currentJobKey: string | null;
  currentJobKind: JobKind | null;
  pendingCount: number;
  pendingJobKeys: string[];
}

export interface MemoryQueueOptions {
  onHighLoad?: (state: QueueHighLoadState) => void | Promise<void>;
  highLoadThreshold?: number;
  highLoadCooldownMs?: number;
  metrics?: Metrics;
}

interface InternalJob {
  payload: JobPayload;
  retries: number;
}

export class MemoryQueue implements IJobQueue {
  private readonly jobs: InternalJob[] = [];
  private handler: JobHandler | null = null;
  private processing = false;
  private stopped = false;
  private currentJobKind: JobKind | null = null;
  private currentJobKey: string | null = null;
  private readonly onHighLoad: MemoryQueueOptions["onHighLoad"];
  private readonly highLoadThreshold: number;
  private readonly highLoadCooldownMs: number;
  private lastHighLoadAt = 0;
  private readonly metrics: Metrics;
  private currentJobStartedAt = 0;
  private sampler: ReturnType<typeof setInterval> | null = null;
  private currentAbort: AbortController | null = null;
  private currentRun: Promise<void> | null = null;

  constructor(
    private readonly logger: Logger,
    options: MemoryQueueOptions = {},
  ) {
    this.onHighLoad = options.onHighLoad;
    this.highLoadThreshold = options.highLoadThreshold ?? 5;
    this.highLoadCooldownMs = options.highLoadCooldownMs ?? HIGH_LOAD_COOLDOWN_MS;
    this.metrics = options.metrics ?? new NoopMetrics();
  }

  getCurrentJobKind(): JobKind | null {
    return this.currentJobKind;
  }

  pendingCount(): number {
    return this.jobs.length;
  }

  getCurrentJobKey(): string | null {
    return this.currentJobKey;
  }

  getPendingJobKeys(): string[] {
    return this.jobs.map((j) => jobLogKey(j.payload));
  }

  async enqueue(job: JobPayload): Promise<void> {
    const key = jobLogKey(job);

    if (this.currentJobKey === key || this.jobs.some((j) => jobLogKey(j.payload) === key)) {
      this.logger.info({ jobKey: key, queueSize: this.jobs.length }, "job_deduplicated");
      this.metrics.increment(METRIC.jobsDeduplicated, { job_kind: job.jobKind });
      return;
    }

    this.jobs.push({ payload: job, retries: 0 });
    this.logger.info({ jobKey: key, queueSize: this.jobs.length }, "job_received");
    this.metrics.increment(METRIC.jobsReceived, { job_kind: job.jobKind });

    if (
      this.onHighLoad &&
      this.jobs.length > this.highLoadThreshold &&
      Date.now() - this.lastHighLoadAt >= this.highLoadCooldownMs
    ) {
      this.lastHighLoadAt = Date.now();
      this.metrics.increment(METRIC.queueHighLoad);
      const state: QueueHighLoadState = {
        currentJobKey: this.getCurrentJobKey(),
        currentJobKind: this.getCurrentJobKind(),
        pendingCount: this.pendingCount(),
        pendingJobKeys: this.getPendingJobKeys(),
      };
      void Promise.resolve(this.onHighLoad(state)).catch((err) => {
        this.logger.warn(
          { error: err instanceof Error ? err.message : String(err) },
          "queue_high_load_alert_failed",
        );
      });
    }

    this.drain();
  }

  start(handler: JobHandler): void {
    this.handler = handler;
    this.stopped = false;
    if (!this.sampler) {
      this.sampler = setInterval(() => this.sample(), OBSERVABILITY.QUEUE_SAMPLE_INTERVAL_MS);
    }
    this.drain();
  }

  stop(): void {
    this.stopped = true;
    this.handler = null;
    if (this.sampler) {
      clearInterval(this.sampler);
      this.sampler = null;
    }
  }

  async shutdown(graceMs: number): Promise<void> {
    this.stopped = true;
    if (this.sampler) {
      clearInterval(this.sampler);
      this.sampler = null;
    }
    if (!this.processing || !this.currentRun) {
      this.handler = null;
      return;
    }

    this.logger.info({ jobKey: this.currentJobKey, graceMs }, "shutdown_draining_in_flight_job");
    const finished = await Promise.race([
      this.currentRun.then(() => true),
      new Promise<false>((r) => {
        setTimeout(() => r(false), graceMs).unref?.();
      }),
    ]);
    if (!finished) {
      this.logger.warn({ jobKey: this.currentJobKey }, "shutdown_grace_expired_aborting_job");
      this.currentAbort?.abort();
      await Promise.race([
        this.currentRun.catch(() => {}),
        new Promise<void>((r) => {
          setTimeout(r, SHUTDOWN.CLEANUP_GRACE_MS).unref?.();
        }),
      ]);
    }
    this.handler = null;
  }

  isBusy(): boolean {
    return this.processing;
  }

  private drain(): void {
    if (this.processing || this.stopped || !this.handler) return;

    const internal = this.jobs.shift();
    if (!internal) return;

    this.processing = true;
    this.currentJobKind = internal.payload.jobKind;
    this.currentJobKey = jobLogKey(internal.payload);
    this.currentJobStartedAt = Date.now();
    this.currentAbort = new AbortController();

    this.currentRun = this.runJob(internal, this.currentAbort.signal).finally(() => {
      this.processing = false;
      this.currentJobKind = null;
      this.currentJobKey = null;
      this.currentAbort = null;
      this.currentRun = null;
      this.drain();
    });
  }

  private async runJob(internal: InternalJob, signal: AbortSignal): Promise<void> {
    const { payload } = internal;
    try {
      const outcome: JobOutcome = await this.executeWithTimeout(payload, signal);
      this.logger.info({ jobKey: jobLogKey(payload), outcome }, "job_completed");
      this.metrics.histogram(METRIC.jobsDuration, Date.now() - this.currentJobStartedAt, {
        job_kind: payload.jobKind,
        outcome: String(outcome),
      });
      this.metrics.increment(METRIC.jobsCompleted, {
        job_kind: payload.jobKind,
        outcome: String(outcome),
      });
    } catch (err) {
      if (err instanceof JobAbortedError) {
        this.logger.warn({ jobKey: jobLogKey(payload) }, "job_aborted_for_shutdown");
        return; // do NOT retry; the source poller re-detects the work on next boot
      }
      if (internal.retries < QUEUE.MAX_RETRIES) {
        internal.retries++;
        this.logger.warn(
          {
            jobKey: jobLogKey(payload),
            retry: internal.retries,
            error: err instanceof Error ? err.message : String(err),
          },
          "retry_attempt",
        );
        this.metrics.increment(METRIC.jobsRetried, { job_kind: payload.jobKind });
        this.jobs.unshift(internal);
      } else {
        this.logger.error(
          {
            jobKey: jobLogKey(payload),
            error: err instanceof Error ? err.message : String(err),
          },
          "job_aborted",
        );
        this.metrics.increment(METRIC.jobsCompleted, {
          job_kind: payload.jobKind,
          outcome: "aborted",
        });
      }
    }
  }

  private sample(): void {
    this.metrics.gauge(METRIC.queueDepth, this.jobs.length);
    this.metrics.gauge(METRIC.queueBusy, this.processing ? 1 : 0);
    const ageSec = this.processing ? Math.round((Date.now() - this.currentJobStartedAt) / 1000) : 0;
    this.metrics.gauge(METRIC.queueCurrentJobAge, ageSec);
  }

  private async executeWithTimeout(job: JobPayload, signal: AbortSignal): Promise<JobOutcome> {
    if (!this.handler) return "completed";

    let timeoutId: ReturnType<typeof setTimeout> | undefined;
    const timeout = new Promise<never>((_, reject) => {
      timeoutId = setTimeout(
        () =>
          reject(
            new JobTimeoutError(`Job ${jobLogKey(job)} timed out after ${QUEUE.JOB_TIMEOUT_MS}ms`),
          ),
        QUEUE.JOB_TIMEOUT_MS,
      );
      if (typeof timeoutId === "object" && timeoutId !== null && "unref" in timeoutId) {
        (timeoutId as unknown as { unref: () => void }).unref();
      }
    });

    const handlerDone = this.handler(job, signal);

    try {
      return await Promise.race([handlerDone, timeout]);
    } catch (err) {
      await Promise.race([
        handlerDone.catch(() => {
          /* swallow: timeout path; cleanup may still throw internally */
        }),
        new Promise((r) => setTimeout(r, SHUTDOWN.CLEANUP_GRACE_MS)),
      ]);
      throw err;
    } finally {
      if (timeoutId !== undefined) clearTimeout(timeoutId);
    }
  }
}
