import { createCoreAPIClient } from "@/lib/features/coreClient";
import type { CoreResponse } from "@/lib/features/types";
import type {
  CreatedPublicLink,
  DashboardAuditEntry,
  DashboardAccessGrant,
  DashboardDetail,
  DashboardListItem,
  DashboardPublicLink,
  DashboardVersion,
  UploadedVersion,
} from "./types";

const coreClient = createCoreAPIClient();

const BASE = "/v1/admin-console/dashboards";

/**
 * Scoped list of dashboards.
 *
 * The backend decides scope FAIL-CLOSED: a super admin or dashboard admin gets
 * everything, a user with explicit grants gets exactly those, and anyone else
 * gets an empty array. The client never filters for authorisation — only for
 * search and display.
 */
export async function listDashboards(
  headers?: HeadersInit,
): Promise<CoreResponse<DashboardListItem[]>> {
  return await coreClient<DashboardListItem[]>(BASE, {
    method: "GET",
    headers,
  });
}

export async function getDashboardDetail(
  id: string,
  headers?: HeadersInit,
): Promise<CoreResponse<DashboardDetail>> {
  return await coreClient<DashboardDetail>(`${BASE}/${id}`, {
    method: "GET",
    headers,
  });
}

export async function createDashboard(input: {
  name: string;
  description?: string | null;
}): Promise<CoreResponse<DashboardListItem>> {
  return await coreClient<DashboardListItem>(BASE, {
    method: "POST",
    body: JSON.stringify(input),
  });
}

export async function updateDashboard(
  id: string,
  patch: {
    name?: string;
    description?: string | null;
    status?: "draft" | "published";
  },
): Promise<CoreResponse<DashboardListItem>> {
  return await coreClient<DashboardListItem>(`${BASE}/${id}`, {
    method: "PATCH",
    body: JSON.stringify(patch),
  });
}

export async function deleteDashboard(
  id: string,
): Promise<CoreResponse<{ id: string }>> {
  return await coreClient<{ id: string }>(`${BASE}/${id}`, {
    method: "DELETE",
  });
}

// ---------------------------------------------------------------------------
// Versions
// ---------------------------------------------------------------------------

/**
 * Multipart bundle upload with progress.
 *
 * Uses XMLHttpRequest rather than fetch purely because fetch still can't report
 * upload progress, and a 50 MB zip needs a progress bar. The envelope is
 * unwrapped by hand here to match what coreClient would have returned, and a
 * 422 carries the Bundle Contract's `errors[]` straight through so the wizard
 * can list every offending file.
 */
export function uploadDashboardVersion(
  id: string,
  file: File,
  onProgress?: (percent: number) => void,
): Promise<CoreResponse<UploadedVersion> & { errors?: string[] }> {
  return new Promise((resolve) => {
    const form = new FormData();
    form.append("bundle", file);

    const xhr = new XMLHttpRequest();
    xhr.open("POST", `/api/proxy${BASE}/${id}/versions`);
    xhr.withCredentials = true;

    xhr.upload.onprogress = (e) => {
      if (e.lengthComputable && onProgress) {
        onProgress(Math.round((e.loaded / e.total) * 100));
      }
    };

    xhr.onload = () => {
      try {
        const body = JSON.parse(xhr.responseText);
        resolve({
          ...body,
          success:
            xhr.status >= 200 && xhr.status < 300 && body?.success !== false,
          // The 422 body puts the contract failures in `error`.
          errors: Array.isArray(body?.error) ? body.error : body?.errors,
        });
      } catch {
        resolve({
          success: false,
          message: `Upload failed (HTTP ${xhr.status})`,
          data: null as never,
        });
      }
    };
    xhr.onerror = () =>
      resolve({
        success: false,
        message: "Upload failed — check your connection and try again.",
        data: null as never,
      });

    xhr.send(form);
  });
}

export async function listDashboardVersions(
  id: string,
  headers?: HeadersInit,
): Promise<CoreResponse<DashboardVersion[]>> {
  return await coreClient<DashboardVersion[]>(`${BASE}/${id}/versions`, {
    method: "GET",
    headers,
  });
}

/** Draft-only: the backend refuses once the dashboard has been published. */
export async function setVersionEntryPoint(
  id: string,
  versionId: string,
  entryPoint: string,
): Promise<CoreResponse<DashboardVersion>> {
  return await coreClient<DashboardVersion>(
    `${BASE}/${id}/versions/${versionId}/entry-point`,
    { method: "PATCH", body: JSON.stringify({ entryPoint }) },
  );
}

export async function publishDashboard(
  id: string,
  versionId: string,
): Promise<CoreResponse<DashboardListItem>> {
  return await coreClient<DashboardListItem>(`${BASE}/${id}/publish`, {
    method: "POST",
    body: JSON.stringify({ versionId }),
  });
}

export async function rollbackDashboard(
  id: string,
  versionId: string,
): Promise<CoreResponse<DashboardListItem>> {
  return await coreClient<DashboardListItem>(`${BASE}/${id}/rollback`, {
    method: "POST",
    body: JSON.stringify({ versionId }),
  });
}

// ---------------------------------------------------------------------------
// Access
// ---------------------------------------------------------------------------

export async function getDashboardAccess(
  id: string,
  headers?: HeadersInit,
): Promise<CoreResponse<DashboardAccessGrant[]>> {
  return await coreClient<DashboardAccessGrant[]>(`${BASE}/${id}/access`, {
    method: "GET",
    headers,
  });
}

/** Replaces the whole allow-list; an empty array revokes everyone. */
export async function setDashboardAccess(
  id: string,
  userIds: string[],
): Promise<
  CoreResponse<{
    accessList: DashboardAccessGrant[];
    added: string[];
    removed: string[];
  }>
> {
  return await coreClient(`${BASE}/${id}/access`, {
    method: "PUT",
    body: JSON.stringify({ userIds }),
  });
}

// ---------------------------------------------------------------------------
// Public links
// ---------------------------------------------------------------------------

export async function listPublicLinks(
  id: string,
  headers?: HeadersInit,
): Promise<CoreResponse<DashboardPublicLink[]>> {
  return await coreClient<DashboardPublicLink[]>(`${BASE}/${id}/public-links`, {
    method: "GET",
    headers,
  });
}

/** The response carries the raw token — shown once, never retrievable again. */
export async function createPublicLink(
  id: string,
  input: { expiresAt?: string | null; password?: string | null },
): Promise<CoreResponse<CreatedPublicLink>> {
  return await coreClient<CreatedPublicLink>(`${BASE}/${id}/public-links`, {
    method: "POST",
    body: JSON.stringify(input),
  });
}

export async function revokePublicLink(
  id: string,
  linkId: string,
): Promise<CoreResponse<{ id: string; isRevoked: boolean }>> {
  return await coreClient(`${BASE}/${id}/public-links/${linkId}`, {
    method: "DELETE",
  });
}

// ---------------------------------------------------------------------------
// Audit + cover
// ---------------------------------------------------------------------------

export async function getDashboardAudit(
  id: string,
  headers?: HeadersInit,
): Promise<CoreResponse<DashboardAuditEntry[]>> {
  return await coreClient<DashboardAuditEntry[]>(`${BASE}/${id}/audit`, {
    method: "GET",
    headers,
  });
}

/**
 * Deliberately not routed through coreClient: it forces
 * `Content-Type: application/json` on every non-GET, which strips the multipart
 * boundary the browser needs to generate. Same reason the bundle upload uses
 * XHR. Browser-only, so /api/proxy is the right base.
 */
export async function uploadDashboardCover(
  id: string,
  file: File,
): Promise<CoreResponse<DashboardListItem>> {
  const form = new FormData();
  form.append("image", file);
  try {
    const res = await fetch(`/api/proxy${BASE}/${id}/cover`, {
      method: "POST",
      body: form,
      credentials: "include",
    });
    const body = await res.json();
    return { ...body, success: res.ok && body?.success !== false };
  } catch (err) {
    return {
      success: false,
      message: (err as Error).message ?? "Cover upload failed",
      data: null as never,
    };
  }
}

/**
 * URL for an <img>. Access-scoped server-side — a dashboard the viewer can't
 * see returns 404, and the card falls back to its generated gradient.
 */
export const dashboardCoverUrl = (id: string) =>
  `/api/proxy${BASE}/${id}/cover`;
