/**
 * 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";

/** User summary for admin dropdown (GET /api/admin/users). */
export interface UserForAdmin {
  id: number;
  full_name: string;
  username: string | null;
  role_id: number | null;
}

/** Minimal duty shape returned by PATCH /api/admin/duties/:id. */
export interface DutyReassignResponse {
  id: number;
  user_id: number;
  start_at: string;
  end_at: string;
}

/** 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 };
}

/**
 * Parse 403 response body for user-facing detail. Returns default i18n message if body is invalid.
 */
async function handle403Response(
  res: Response,
  acceptLang: ApiLang,
  defaultI18nKey: string
): Promise<string> {
  let detail = translate(acceptLang, defaultI18nKey);
  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 */
  }
  return detail;
}

/**
 * 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);
      const detail = await handle403Response(res, acceptLang, "access_denied");
      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");
      const detail = await handle403Response(res, acceptLang, "access_denied");
      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();
  }
}

/**
 * Fetch admin status for the current user (GET /api/admin/me).
 * Returns { is_admin: true } or { is_admin: false }. On 403, treat as not admin.
 */
export async function fetchAdminMe(
  initData: string,
  acceptLang: ApiLang
): Promise<{ is_admin: boolean }> {
  const base =
    typeof window !== "undefined" ? window.location.origin : "";
  const url = `${base}/api/admin/me`;
  const opts = buildFetchOptions(initData, acceptLang);
  try {
    logger.debug("API request", "/api/admin/me");
    const res = await fetch(url, { headers: opts.headers, signal: opts.signal });
    if (res.status === 403) {
      return { is_admin: false };
    }
    if (!res.ok) {
      return { is_admin: false };
    }
    const data = (await res.json()) as { is_admin?: boolean };
    return {
      is_admin: typeof data?.is_admin === "boolean" ? data.is_admin : false,
    };
  } catch (e) {
    logger.warn("API request failed", "/api/admin/me", e);
    return { is_admin: false };
  } finally {
    opts.cleanup();
  }
}

/**
 * Fetch users for admin dropdown (GET /api/admin/users). Admin only; throws AccessDeniedError on 403.
 */
export async function fetchAdminUsers(
  initData: string,
  acceptLang: ApiLang,
  signal?: AbortSignal | null
): Promise<UserForAdmin[]> {
  const base =
    typeof window !== "undefined" ? window.location.origin : "";
  const url = `${base}/api/admin/users`;
  const opts = buildFetchOptions(initData, acceptLang, signal);
  try {
    logger.debug("API request", "/api/admin/users");
    const res = await fetch(url, { headers: opts.headers, signal: opts.signal });
    if (res.status === 403) {
      const detail = await handle403Response(res, acceptLang, "admin.access_denied");
      throw new AccessDeniedError(API_ACCESS_DENIED, detail);
    }
    if (!res.ok) {
      throw new Error(translate(acceptLang, "error_load_failed"));
    }
    const data = await res.json();
    if (!Array.isArray(data)) return [];
    return data.filter(
      (x: unknown): x is UserForAdmin =>
        x != null &&
        typeof (x as UserForAdmin).id === "number" &&
        typeof (x as UserForAdmin).full_name === "string" &&
        ((x as UserForAdmin).username === null ||
          typeof (x as UserForAdmin).username === "string") &&
        ((x as UserForAdmin).role_id === null ||
          typeof (x as UserForAdmin).role_id === "number")
    );
  } catch (e) {
    if ((e as Error).name === "AbortError" || e instanceof AccessDeniedError) {
      throw e;
    }
    logger.error("API request failed", "/api/admin/users", e);
    throw e;
  } finally {
    opts.cleanup();
  }
}

/**
 * Reassign a duty to another user (PATCH /api/admin/duties/:id). Admin only.
 * Returns updated duty (id, user_id, start_at, end_at). Throws on 403/404/400.
 */
export async function patchAdminDuty(
  dutyId: number,
  userId: number,
  initData: string,
  acceptLang: ApiLang
): Promise<DutyReassignResponse> {
  const base =
    typeof window !== "undefined" ? window.location.origin : "";
  const url = `${base}/api/admin/duties/${dutyId}`;
  const opts = buildFetchOptions(initData, acceptLang);
  try {
    logger.debug("API request", "PATCH", url, { user_id: userId });
    const res = await fetch(url, {
      method: "PATCH",
      headers: {
        ...opts.headers,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ user_id: userId }),
      signal: opts.signal,
    });
    if (res.status === 403) {
      const detail = await handle403Response(res, acceptLang, "admin.access_denied");
      throw new AccessDeniedError(API_ACCESS_DENIED, detail);
    }
    const data = await res.json().catch(() => ({}));
    if (!res.ok) {
      const msg =
        data && typeof (data as { detail?: string }).detail === "string"
          ? (data as { detail: string }).detail
          : translate(acceptLang, "error_generic");
      throw new Error(msg);
    }
    const out = data as DutyReassignResponse;
    if (
      typeof out?.id !== "number" ||
      typeof out?.user_id !== "number" ||
      typeof out?.start_at !== "string" ||
      typeof out?.end_at !== "string"
    ) {
      throw new Error(translate(acceptLang, "error_load_failed"));
    }
    return out;
  } finally {
    opts.cleanup();
  }
}
