import {
  type AdminLoginInput,
  type AdminUserView,
  type ApiErrorResponse,
  type CorrectionAdminView,
  ErrorCode,
  type MetricsView,
  type MonitoringView,
  type RetrainRequestInput,
  type RetrainResultView,
  type TrainingDataSnippetView,
  type TrainingSuggestionAdminView,
  type TrainingSuggestionGroupView,
  type UpdateTrainingSuggestionInput,
} from "@batch-cooking/shared";

/** Builds a `?a=b&c=d` string from defined values only. */
function query(params: Record<string, string | undefined>): string {
  const entries = Object.entries(params).filter(
    (entry): entry is [string, string] => entry[1] !== undefined && entry[1] !== "",
  );
  return entries.length === 0 ? "" : `?${new URLSearchParams(entries).toString()}`;
}

/**
 * Base URL of the admin API surface, configurable via `VITE_ADMIN_API_URL`
 * (see `.env.example`). Defaults to `""` (same origin) — correct behind a
 * shared reverse proxy; native dev overrides it to `http://localhost:3000`
 * in `apps/admin-web/.env` since the Vite dev server (5174) and the API
 * (3000) are different origins.
 */
const ADMIN_API_BASE_URL: string = import.meta.env.VITE_ADMIN_API_URL ?? "";

/**
 * Thrown by {@link AdminApiClient} on any non-2xx response — carries the
 * same {@link ErrorCode} the API returned. Same shape as apps/web's
 * `ApiError`; kept separate rather than shared so the two apps' transport
 * layers stay independent.
 */
export class ApiError extends Error {
  public readonly status: number;
  public readonly code: ErrorCode;
  public readonly fieldErrors?: Record<string, string[] | undefined>;

  public constructor(status: number, body: ApiErrorResponse) {
    super(body.message);
    this.name = "ApiError";
    this.status = status;
    this.code = body.code;
    this.fieldErrors = body.details;
  }
}

/**
 * Thin fetch wrapper around the `/admin/*` endpoints — same design as
 * apps/web's `ApiClient` (a class for cohesion/extensibility, one shared
 * stateless instance). Every request sends credentials so the
 * `admin_session` httpOnly cookie round-trips.
 */
export class AdminApiClient {
  /**
   * Performs a JSON request against the admin API and returns the parsed body.
   *
   * @throws {ApiError} if the response status is not in the 2xx range.
   */
  private async _request<TResponseBody>(
    path: string,
    options: RequestInit = {},
  ): Promise<TResponseBody> {
    try {
      const response = await fetch(`${ADMIN_API_BASE_URL}${path}`, {
        ...options,
        credentials: "include",
        headers: { "Content-Type": "application/json", ...options.headers },
      });

      if (!response.ok) {
        const body = (await response.json().catch(() => null)) as ApiErrorResponse | null;
        throw new ApiError(
          response.status,
          body ?? { code: ErrorCode.INTERNAL_ERROR, message: "Something went wrong" },
        );
      }

      if (response.status === 204) {
        return undefined as TResponseBody;
      }
      return (await response.json()) as TResponseBody;
    } catch (err) {
      // Rethrown as-is — callers surface it their own way; this is just the
      // one place the fetch/`await` sits in a try/catch per the repo's rule.
      throw err;
    }
  }

  /** Verifies admin credentials and starts an admin session. */
  public login(input: AdminLoginInput): Promise<AdminUserView> {
    return this._request("/admin/auth/login", { method: "POST", body: JSON.stringify(input) });
  }

  /** Ends the current admin session. */
  public logout(): Promise<void> {
    return this._request("/admin/auth/logout", { method: "POST" });
  }

  /** Fetches the currently authenticated admin — rejects with `NOT_AUTHENTICATED` if there's no session. */
  public me(): Promise<AdminUserView> {
    return this._request("/admin/auth/me");
  }

  /** Usage metrics for the dashboard — snapshot totals + `days` (7–365) of daily time series. */
  public getMetrics(days: number): Promise<MetricsView> {
    return this._request(`/admin/metrics?days=${days}`);
  }

  /** Live health of Postgres, the API, the intent-service and the LLM worker — polled by the monitoring board. */
  public getMonitoring(): Promise<MonitoringView> {
    return this._request("/admin/monitoring");
  }

  /** Training suggestions, grouped by technique, filtered by the given (all-optional) criteria. */
  public getSuggestions(filters: {
    status?: string;
    sourceType?: string;
    techStepKey?: string;
    locale?: string;
  }): Promise<TrainingSuggestionGroupView[]> {
    return this._request(`/admin/tech-steps/suggestions${query(filters)}`);
  }

  /** Raw user corrections, including the "no technique here" removals. */
  public getCorrections(filters: {
    consumed?: string;
    hasCorrectedTechStep?: string;
  }): Promise<CorrectionAdminView[]> {
    return this._request(`/admin/tech-steps/corrections${query(filters)}`);
  }

  /** Edits a suggestion's proposed synonyms/utterances and/or its status. */
  public updateSuggestion(
    id: number,
    body: UpdateTrainingSuggestionInput,
  ): Promise<TrainingSuggestionAdminView> {
    return this._request(`/admin/tech-steps/suggestions/${id}`, {
      method: "PATCH",
      body: JSON.stringify(body),
    });
  }

  /** The ready-to-paste `training_data.py` block aggregating suggestions for one technique/locale/status. */
  public getTrainingDataSnippet(params: {
    techStepKey: string;
    locale?: string;
    status?: string;
  }): Promise<TrainingDataSnippetView> {
    return this._request(`/admin/tech-steps/training-data-snippet${query(params)}`);
  }

  /** Runs the F1 gate + backfill (+ marks suggestion ids). Rejects with `RETRAIN_ALREADY_RUNNING` if one is in flight. */
  public retrain(body: RetrainRequestInput): Promise<RetrainResultView> {
    return this._request("/admin/tech-steps/retrain", {
      method: "POST",
      body: JSON.stringify(body),
    });
  }
}

/** Single shared instance — stateless, same reasoning as apps/web's `apiClient`. */
export const adminApiClient = new AdminApiClient();
