import { normalizeAuthorityBaseUrl } from "../runtime/authority-capabilities.js";
import { AuthorityHttpClient } from "../runtime/authority-http-client.js";

export const AUTHORITY_JOB_ENDPOINT = "/jobs";
export const AUTHORITY_JOB_STATUS_TERMINAL = new Set([
  "completed",
  "succeeded",
  "success",
  "failed",
  "error",
  "cancelled",
  "canceled",
  "timeout",
]);
export const AUTHORITY_JOB_STATUS_SUCCESS = new Set([
  "completed",
  "succeeded",
  "success",
]);

function toPlainData(value, fallbackValue = null) {
  if (value == null) return fallbackValue;
  if (typeof globalThis.structuredClone === "function") {
    try {
      return globalThis.structuredClone(value);
    } catch {
    }
  }
  try {
    return JSON.parse(JSON.stringify(value));
  } catch {
    return fallbackValue;
  }
}

function normalizeRecordId(value) {
  return String(value ?? "").trim();
}

function normalizeInteger(value, fallback = 0, min = 0, max = Number.MAX_SAFE_INTEGER) {
  const parsed = Number(value);
  if (!Number.isFinite(parsed)) return fallback;
  return Math.min(max, Math.max(min, Math.trunc(parsed)));
}

function normalizeProgress(value = null) {
  if (typeof value === "number") {
    return Math.max(0, Math.min(1, value > 1 ? value / 100 : value));
  }
  if (!value || typeof value !== "object" || Array.isArray(value)) return 0;
  const direct = Number(value.progress ?? value.ratio ?? value.percent);
  if (Number.isFinite(direct)) {
    return Math.max(0, Math.min(1, direct > 1 ? direct / 100 : direct));
  }
  const current = Number(value.current ?? value.done ?? value.completed ?? value.processed);
  const total = Number(value.total ?? value.count ?? value.expected);
  if (Number.isFinite(current) && Number.isFinite(total) && total > 0) {
    return Math.max(0, Math.min(1, current / total));
  }
  return 0;
}

function normalizeJobStatus(value = "queued") {
  return String(value || "queued").trim().toLowerCase() || "queued";
}

function hasAsyncIterator(value = null) {
  return !!value && typeof value[Symbol.asyncIterator] === "function";
}

function hasIterator(value = null) {
  return !!value && typeof value[Symbol.iterator] === "function";
}

function readJobStreamPayload(event = null) {
  const source = event && typeof event === "object" && !Array.isArray(event) ? event : null;
  if (!source) return event;
  if (source.job && typeof source.job === "object" && !Array.isArray(source.job)) {
    return source.job;
  }
  if (source.result && typeof source.result === "object" && !Array.isArray(source.result)) {
    return source.result;
  }
  if (source.payload && typeof source.payload === "object" && !Array.isArray(source.payload)) {
    return source.payload;
  }
  if (source.data && typeof source.data === "object" && !Array.isArray(source.data)) {
    return source.data;
  }
  return source;
}

async function* normalizeAuthorityJobStream(source = null, options = {}) {
  if (hasAsyncIterator(source)) {
    for await (const event of source) {
      throwIfAborted(options.signal);
      const job = normalizeAuthorityJobRecord(readJobStreamPayload(event));
      if (job.id || job.status || job.error) {
        yield job;
      }
    }
    return;
  }
  if (hasIterator(source)) {
    for (const event of source) {
      throwIfAborted(options.signal);
      const job = normalizeAuthorityJobRecord(readJobStreamPayload(event));
      if (job.id || job.status || job.error) {
        yield job;
      }
    }
    return;
  }
  throw new Error("Authority Jobs stream unavailable");
}

function readJobRows(payload = null) {
  if (Array.isArray(payload)) return payload;
  if (!payload || typeof payload !== "object") return [];
  if (Array.isArray(payload.jobs)) return payload.jobs;
  if (Array.isArray(payload.items)) return payload.items;
  if (Array.isArray(payload.rows)) return payload.rows;
  if (Array.isArray(payload.data)) return payload.data;
  if (Array.isArray(payload.result?.jobs)) return payload.result.jobs;
  if (Array.isArray(payload.result?.items)) return payload.result.items;
  return [];
}

export function normalizeAuthorityJobRecord(input = null) {
  const source = input && typeof input === "object" && !Array.isArray(input) ? input : {};
  const id = normalizeRecordId(source.id || source.jobId || source.job_id || source.key);
  const status = normalizeJobStatus(source.status || source.state || source.phase);
  const progress = normalizeProgress(source.progress ?? source.progressRatio ?? source.percent);
  const kind = String(source.kind || source.type || source.name || "").trim();
  const lastEvent = source.lastEvent && typeof source.lastEvent === "object"
    ? toPlainData(source.lastEvent, null)
    : source.event && typeof source.event === "object"
      ? toPlainData(source.event, null)
      : null;
  return {
    id,
    kind,
    status,
    progress,
    terminal: AUTHORITY_JOB_STATUS_TERMINAL.has(status),
    success: AUTHORITY_JOB_STATUS_SUCCESS.has(status),
    error: String(source.error || source.lastError || source.message || ""),
    idempotencyKey: String(source.idempotencyKey || source.idempotency_key || ""),
    queue: String(source.queue || source.worker || ""),
    createdAt: source.createdAt || source.created_at || source.enqueuedAt || "",
    updatedAt: source.updatedAt || source.updated_at || source.finishedAt || "",
    lastEvent,
    raw: toPlainData(source, source),
  };
}

export function normalizeAuthorityJobList(payload = null) {
  const source = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
  const jobs = readJobRows(payload).map(normalizeAuthorityJobRecord).filter((job) => job.id);
  return {
    jobs,
    nextCursor: String(source.nextCursor || source.next_cursor || source.page?.nextCursor || source.cursor?.next || ""),
    hasMore: Boolean(source.hasMore || source.has_more || source.page?.hasMore || source.cursor?.hasMore),
    raw: toPlainData(payload, payload),
  };
}

export function normalizeAuthorityRecentJobRecord(input = null, options = {}) {
  const normalized = normalizeAuthorityJobRecord(input);
  const queueState = String(options.queueState || "").trim() ||
    (normalized.error
      ? "error"
      : normalized.terminal
        ? normalized.success
          ? "success"
          : "failed"
        : normalized.id
          ? "running"
          : "idle");
  return {
    ...normalized,
    updatedAt: String(normalized.updatedAt || options.updatedAt || ""),
    queueState,
  };
}

export function mergeAuthorityRecentJobs(existingJobs = [], incomingJobs = [], options = {}) {
  const limit = normalizeInteger(options.limit, 8, 1, 100);
  const updatedAt = String(options.updatedAt || "");
  const normalizedExisting = Array.isArray(existingJobs) ? existingJobs : [];
  const normalizedIncoming = Array.isArray(incomingJobs) ? incomingJobs : [incomingJobs];
  const seen = new Set();
  const merged = [];

  for (const item of normalizedIncoming) {
    const job = normalizeAuthorityRecentJobRecord(item, { updatedAt });
    if (!job.id || seen.has(job.id)) continue;
    seen.add(job.id);
    merged.push(job);
    if (merged.length >= limit) {
      return merged;
    }
  }

  for (const item of normalizedExisting) {
    const job = normalizeAuthorityRecentJobRecord(item);
    if (!job.id || seen.has(job.id)) continue;
    seen.add(job.id);
    merged.push(job);
    if (merged.length >= limit) {
      break;
    }
  }

  return merged;
}

export function buildAuthorityJobIdempotencyKey({
  kind = "job",
  chatId = "",
  collectionId = "",
  revision = 0,
  range = null,
  suffix = "",
} = {}) {
  const normalizedRange = range && Number.isFinite(Number(range.start)) && Number.isFinite(Number(range.end))
    ? `${Math.min(Number(range.start), Number(range.end))}-${Math.max(Number(range.start), Number(range.end))}`
    : "all";
  return [
    "st-bme",
    normalizeRecordId(kind) || "job",
    normalizeRecordId(chatId) || "unknown-chat",
    normalizeRecordId(collectionId) || "unknown-collection",
    String(Math.max(0, Math.floor(Number(revision) || 0))),
    normalizedRange,
    normalizeRecordId(suffix),
  ].filter(Boolean).join(":");
}

export function normalizeAuthorityJobConfig(settings = {}, overrides = {}) {
  const source = settings && typeof settings === "object" && !Array.isArray(settings) ? settings : {};
  return {
    baseUrl: normalizeAuthorityBaseUrl(source.authorityBaseUrl ?? source.baseUrl),
    enabled: source.authorityJobsEnabled !== false && source.jobsEnabled !== false,
    failOpen: source.authorityFailOpen !== false && source.failOpen !== false,
    preferStream: source.authorityJobPreferStream !== false && source.jobStreamPreferred !== false,
    pollIntervalMs: normalizeInteger(source.authorityJobPollIntervalMs ?? source.pollIntervalMs, 1200, 250, 30000),
    pollMaxIntervalMs: normalizeInteger(source.authorityJobPollMaxIntervalMs ?? source.pollMaxIntervalMs, 5000, 250, 60000),
    pollBackoffFactor: Math.min(5, Math.max(1, Number(source.authorityJobPollBackoffFactor ?? source.pollBackoffFactor ?? 1.25) || 1.25)),
    waitTimeoutMs: normalizeInteger(source.authorityJobWaitTimeoutMs ?? source.waitTimeoutMs, 0, 0, 3600000),
    ...overrides,
  };
}

export class AuthorityJobHttpClient {
  constructor(options = {}) {
    this.http = new AuthorityHttpClient({
      ...options,
      baseUrl: normalizeAuthorityBaseUrl(options.baseUrl),
    });
  }

  async request(path, payload = {}, options = {}) {
    return await this.http.requestJson(path, {
      method: options.method || "POST",
      body: payload,
      session: true,
      signal: options.signal,
    });
  }

  async submit(payload = {}) {
    return await this.request(`${AUTHORITY_JOB_ENDPOINT}/create`, {
      type: String(payload.type || payload.kind || "").trim(),
      payload: toPlainData(payload.payload, payload.payload),
      ...(payload.timeoutMs != null ? { timeoutMs: normalizeInteger(payload.timeoutMs, 0, 0, 3600000) } : {}),
      ...(payload.idempotencyKey ? { idempotencyKey: String(payload.idempotencyKey) } : {}),
      ...(payload.maxAttempts != null ? { maxAttempts: normalizeInteger(payload.maxAttempts, 1, 1, 1000) } : {}),
    });
  }

  async listPage(payload = {}) {
    return await this.request(`${AUTHORITY_JOB_ENDPOINT}/list`, {
      page: {
        ...(payload.cursor ? { cursor: String(payload.cursor) } : {}),
        limit: normalizeInteger(payload.limit, 20, 1, 100),
      },
      ...(payload.filter && typeof payload.filter === "object" && !Array.isArray(payload.filter) && Object.keys(payload.filter).length > 0
        ? { filter: toPlainData(payload.filter, {}) }
        : {}),
    });
  }

  async get(payload = {}) {
    const id = encodeURIComponent(normalizeRecordId(payload.jobId || payload.id));
    if (!id) {
      throw new Error("Authority Jobs get requires job id");
    }
    return await this.request(`${AUTHORITY_JOB_ENDPOINT}/${id}`, undefined, {
      method: "GET",
      signal: payload.signal,
    });
  }

  async requeue(payload = {}) {
    const id = encodeURIComponent(normalizeRecordId(payload.jobId || payload.id));
    if (!id) {
      throw new Error("Authority Jobs requeue requires job id");
    }
    return await this.request(`${AUTHORITY_JOB_ENDPOINT}/${id}/requeue`, {}, { signal: payload.signal });
  }

  async cancel(payload = {}) {
    const id = encodeURIComponent(normalizeRecordId(payload.jobId || payload.id));
    if (!id) {
      throw new Error("Authority Jobs cancel requires job id");
    }
    return await this.request(`${AUTHORITY_JOB_ENDPOINT}/${id}/cancel`, {}, { signal: payload.signal });
  }
}

export function createAuthorityJobClient(config = {}, options = {}) {
  const injected = options.jobClient || config.jobClient || globalThis.__stBmeAuthorityJobClient;
  if (injected) return injected;
  return new AuthorityJobHttpClient({
    baseUrl: config.baseUrl,
    fetchImpl: options.fetchImpl || config.fetchImpl,
    headerProvider: options.headerProvider || config.headerProvider,
  });
}

async function callClient(client, methodNames = [], action = "request", payload = {}) {
  for (const methodName of methodNames) {
    if (typeof client?.[methodName] === "function") {
      return await client[methodName](payload);
    }
  }
  if (typeof client?.request === "function") {
    return await client.request(action, payload);
  }
  if (typeof client === "function") {
    return await client({ action, ...payload });
  }
  throw new Error(`Authority Jobs ${action} unavailable`);
}

async function callStreamClient(client, methodNames = [], payload = {}) {
  for (const methodName of methodNames) {
    if (typeof client?.[methodName] === "function") {
      return await client[methodName](payload);
    }
  }
  throw new Error("Authority Jobs stream unavailable");
}

function throwIfAborted(signal) {
  if (signal?.aborted) {
    throw signal.reason instanceof Error
      ? signal.reason
      : Object.assign(new Error("操作已终止"), { name: "AbortError" });
  }
}

function sleep(ms, signal) {
  if (!Number.isFinite(Number(ms)) || Number(ms) <= 0) return Promise.resolve();
  return new Promise((resolve, reject) => {
    const timer = setTimeout(resolve, Math.max(0, Math.floor(Number(ms))));
    if (signal) {
      signal.addEventListener(
        "abort",
        () => {
          clearTimeout(timer);
          reject(signal.reason instanceof Error ? signal.reason : Object.assign(new Error("操作已终止"), { name: "AbortError" }));
        },
        { once: true },
      );
    }
  });
}

export class AuthorityJobAdapter {
  constructor(config = {}, options = {}) {
    this.config = normalizeAuthorityJobConfig(config, options.configOverrides || {});
    this.client = createAuthorityJobClient(this.config, options);
  }

  async submit(kind, payload = {}, options = {}) {
    throwIfAborted(options.signal);
    const result = await callClient(this.client, ["submit", "enqueue"], "submit", {
      kind,
      type: kind,
      idempotencyKey: options.idempotencyKey || payload.idempotencyKey || "",
      payload,
    });
    return normalizeAuthorityJobRecord(result?.job || result?.result || result);
  }

  async listPage(options = {}) {
    throwIfAborted(options.signal);
    const result = await callClient(this.client, ["listPage", "list"], "listPage", {
      cursor: options.cursor || "",
      limit: normalizeInteger(options.limit, 20, 1, 100),
      filter: options.filter || {},
    });
    return normalizeAuthorityJobList(result);
  }

  async get(jobId, options = {}) {
    throwIfAborted(options.signal);
    const id = normalizeRecordId(jobId);
    if (!id) return normalizeAuthorityJobRecord(null);
    const result = await callClient(this.client, ["get", "status"], "get", { jobId: id, id });
    return normalizeAuthorityJobRecord(result?.job || result?.result || result);
  }

  async stream(jobId, options = {}) {
    throwIfAborted(options.signal);
    const id = normalizeRecordId(jobId);
    if (!id) {
      return normalizeAuthorityJobStream([], options);
    }
    const source = await callStreamClient(
      this.client,
      ["stream", "streamJob", "watch", "watchJob"],
      {
        jobId: id,
        id,
        signal: options.signal,
      },
    );
    return normalizeAuthorityJobStream(source, options);
  }

  async waitForCompletion(jobId, options = {}) {
    throwIfAborted(options.signal);
    const id = normalizeRecordId(jobId);
    if (!id) return normalizeAuthorityJobRecord(null);
    if (typeof this.client?.waitForCompletion === "function") {
      const result = await this.client.waitForCompletion({
        jobId: id,
        id,
        timeoutMs: normalizeInteger(options.timeoutMs, this.config.waitTimeoutMs, 0, 3600000),
      });
      const normalized = normalizeAuthorityJobRecord(result?.job || result?.result || result);
      return {
        ...normalized,
        waitDiagnostics: {
          mode: "client",
          pollCount: 0,
          elapsedMs: 0,
          timeoutMs: normalizeInteger(options.timeoutMs, this.config.waitTimeoutMs, 0, 3600000),
          terminal: normalized.terminal,
        },
      };
    }

    const startedAt = Date.now();
    const timeoutMs = normalizeInteger(options.timeoutMs, this.config.waitTimeoutMs, 0, 3600000);
    const initialPollIntervalMs = normalizeInteger(options.pollIntervalMs, this.config.pollIntervalMs, 250, 30000);
    const maxPollIntervalMs = Math.max(
      initialPollIntervalMs,
      normalizeInteger(options.pollMaxIntervalMs, this.config.pollMaxIntervalMs, 250, 60000),
    );
    const backoffFactor = Math.min(5, Math.max(1, Number(options.pollBackoffFactor ?? this.config.pollBackoffFactor) || 1));
    let pollIntervalMs = initialPollIntervalMs;
    let pollCount = 0;
    let lastJob = normalizeAuthorityJobRecord(null);
    while (true) {
      throwIfAborted(options.signal);
      const job = await this.get(id, options);
      pollCount += 1;
      lastJob = job;
      const elapsedMs = Date.now() - startedAt;
      if (job.terminal) {
        return {
          ...job,
          waitDiagnostics: {
            mode: "poll",
            pollCount,
            elapsedMs,
            timeoutMs,
            pollIntervalMs: initialPollIntervalMs,
            maxPollIntervalMs,
            backoffFactor,
            terminal: true,
          },
        };
      }
      if (timeoutMs > 0 && elapsedMs >= timeoutMs) {
        return {
          ...job,
          status: "timeout",
          terminal: true,
          success: false,
          error: "wait timeout",
          waitDiagnostics: {
            mode: "poll",
            pollCount,
            elapsedMs,
            timeoutMs,
            pollIntervalMs: initialPollIntervalMs,
            maxPollIntervalMs,
            backoffFactor,
            terminal: false,
            lastStatus: lastJob.status,
            lastProgress: lastJob.progress,
          },
        };
      }
      const remainingMs = timeoutMs > 0 ? Math.max(0, timeoutMs - elapsedMs) : pollIntervalMs;
      await sleep(timeoutMs > 0 ? Math.min(pollIntervalMs, remainingMs) : pollIntervalMs, options.signal);
      pollIntervalMs = Math.min(maxPollIntervalMs, Math.max(initialPollIntervalMs, Math.ceil(pollIntervalMs * backoffFactor)));
    }
  }

  async requeue(jobId, options = {}) {
    throwIfAborted(options.signal);
    const id = normalizeRecordId(jobId);
    const result = await callClient(this.client, ["requeue", "safeRequeue"], "requeue", {
      jobId: id,
      id,
      safe: options.safe !== false,
    });
    return normalizeAuthorityJobRecord(result?.job || result?.result || result);
  }

  async cancel(jobId, options = {}) {
    throwIfAborted(options.signal);
    const id = normalizeRecordId(jobId);
    const result = await callClient(this.client, ["cancel", "cancelLike"], "cancel", {
      jobId: id,
      id,
    });
    return normalizeAuthorityJobRecord(result?.job || result?.result || result);
  }
}

export function createAuthorityJobAdapter(config = {}, options = {}) {
  return new AuthorityJobAdapter(config, options);
}
