/**
 * API layer: fetch duties and calendar events with auth header and error handling.
 * Ported from webapp/js/api.js. Uses fetch with X-Telegram-Init-Data and Accept-Language.
 * No Next.js fetch caching — client-side SPA with static export.
 */

import { FETCH_TIMEOUT_MS } from "./constants";
import { logger } from "./logger";
import type { DutyWithUser, CalendarEvent } from "@/types";
import { translate } from "@/i18n/messages";

type ApiLang = "ru" | "en";

/** Minimal runtime check for a single duty item (required fields). */
function isDutyWithUser(x: unknown): x is DutyWithUser {
  if (!x || typeof x !== "object") return false;
  const o = x as Record<string, unknown>;
  return (
    typeof o.id === "number" &&
    typeof o.user_id === "number" &&
    typeof o.start_at === "string" &&
    typeof o.end_at === "string" &&
    typeof o.full_name === "string" &&
    (o.event_type === "duty" || o.event_type === "unavailable" || o.event_type === "vacation") &&
    (o.phone === null || typeof o.phone === "string") &&
    (o.username === null || typeof o.username === "string")
  );
}

/** Minimal runtime check for a single calendar event. */
function isCalendarEvent(x: unknown): x is CalendarEvent {
  if (!x || typeof x !== "object") return false;
  const o = x as Record<string, unknown>;
  return typeof o.date === "string" && typeof o.summary === "string";
}

function validateDuties(data: unknown, acceptLang: ApiLang): DutyWithUser[] {
  if (!Array.isArray(data)) {
    logger.warn("API response is not an array (duties)", typeof data);
    throw new Error(translate(acceptLang, "error_load_failed"));
  }
  for (let i = 0; i < data.length; i++) {
    if (!isDutyWithUser(data[i])) {
      logger.warn("API duties item invalid at index", i, data[i]);
      throw new Error(translate(acceptLang, "error_load_failed"));
    }
  }
  return data as DutyWithUser[];
}

function validateCalendarEvents(data: unknown): CalendarEvent[] {
  if (!Array.isArray(data)) {
    logger.warn("API response is not an array (calendar-events)", typeof data);
    return [];
  }
  const out: CalendarEvent[] = [];
  for (let i = 0; i < data.length; i++) {
    if (isCalendarEvent(data[i])) {
      out.push(data[i]);
    } else {
      logger.warn("API calendar-events item invalid at index", i, data[i]);
    }
  }
  return out;
}

const API_ACCESS_DENIED = "ACCESS_DENIED";

/** Error thrown on 403 with server detail attached. */
export class AccessDeniedError extends Error {
  constructor(
    message: string = API_ACCESS_DENIED,
    public serverDetail?: string
  ) {
    super(message);
    this.name = "AccessDeniedError";
  }
}

/**
 * Build fetch options with init data header, Accept-Language and timeout abort.
 * Optional external signal aborts this request when triggered.
 */
function buildFetchOptions(
  initData: string,
  acceptLang: ApiLang,
  externalSignal?: AbortSignal | null
): { headers: HeadersInit; signal: AbortSignal; cleanup: () => void } {
  const headers: Record<string, string> = {
    "Accept-Language": acceptLang || "en",
  };
  if (initData) {
    headers["X-Telegram-Init-Data"] = initData;
  }
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
  const onAbort = (): void => controller.abort();
  const cleanup = (): void => {
    clearTimeout(timeoutId);
    if (externalSignal) {
      externalSignal.removeEventListener("abort", onAbort);
    }
  };
  if (externalSignal) {
    if (externalSignal.aborted) {
      cleanup();
      controller.abort();
    } else {
      externalSignal.addEventListener("abort", onAbort);
    }
  }
  return { headers, signal: controller.signal, cleanup };
}

/**
 * Fetch duties for date range. Throws AccessDeniedError on 403.
 * Rethrows AbortError when the request is cancelled (e.g. stale load).
 */
export async function fetchDuties(
  from: string,
  to: string,
  initData: string,
  acceptLang: ApiLang,
  signal?: AbortSignal | null
): Promise<DutyWithUser[]> {
  const base =
    typeof window !== "undefined" ? window.location.origin : "";
  const url = `${base}/api/duties?${new URLSearchParams({ from, to }).toString()}`;
  const opts = buildFetchOptions(initData, acceptLang, signal);
  try {
    logger.debug("API request", "/api/duties", { from, to });
    const res = await fetch(url, { headers: opts.headers, signal: opts.signal });
    if (res.status === 403) {
      logger.warn("Access denied", from, to);
      let detail = translate(acceptLang, "access_denied");
      try {
        const body = await res.json();
        if (body && (body as { detail?: string }).detail !== undefined) {
          const d = (body as { detail: string | { msg?: string } }).detail;
          detail =
            typeof d === "string" ? d : (d.msg ?? JSON.stringify(d));
        }
      } catch {
        /* ignore */
      }
      throw new AccessDeniedError(API_ACCESS_DENIED, detail);
    }
    if (!res.ok) {
      throw new Error(translate(acceptLang, "error_load_failed"));
    }
    const data = await res.json();
    return validateDuties(data, acceptLang);
  } catch (e) {
    if ((e as Error).name === "AbortError" || e instanceof AccessDeniedError) {
      throw e;
    }
    logger.error("API request failed", "/api/duties", e);
    throw e;
  } finally {
    opts.cleanup();
  }
}

/**
 * Fetch calendar events for range. Throws AccessDeniedError on 403.
 * Returns [] on other non-200. Rethrows AbortError when the request is cancelled.
 */
export async function fetchCalendarEvents(
  from: string,
  to: string,
  initData: string,
  acceptLang: ApiLang,
  signal?: AbortSignal | null
): Promise<CalendarEvent[]> {
  const base =
    typeof window !== "undefined" ? window.location.origin : "";
  const url = `${base}/api/calendar-events?${new URLSearchParams({ from, to }).toString()}`;
  const opts = buildFetchOptions(initData, acceptLang, signal);
  try {
    logger.debug("API request", "/api/calendar-events", { from, to });
    const res = await fetch(url, { headers: opts.headers, signal: opts.signal });
    if (res.status === 403) {
      logger.warn("Access denied", from, to, "calendar-events");
      let detail = translate(acceptLang, "access_denied");
      try {
        const body = await res.json();
        if (body && (body as { detail?: string }).detail !== undefined) {
          const d = (body as { detail: string | { msg?: string } }).detail;
          detail =
            typeof d === "string" ? d : (d.msg ?? JSON.stringify(d));
        }
      } catch {
        /* ignore */
      }
      throw new AccessDeniedError(API_ACCESS_DENIED, detail);
    }
    if (!res.ok) return [];
    const data = await res.json();
    return validateCalendarEvents(data);
  } catch (e) {
    if ((e as Error).name === "AbortError" || e instanceof AccessDeniedError) {
      throw e;
    }
    logger.error("API request failed", "/api/calendar-events", e);
    return [];
  } finally {
    opts.cleanup();
  }
}
