// ---------------------------------------------------------------------------
// Ory Kratos browser client
// ---------------------------------------------------------------------------
// Identity is owned by Ory Kratos. The browser talks to the Kratos public API
// directly using its self-service browser flows. Every request must include
// credentials so the flow + `ory_kratos_session` cookies are sent.
//
// Flow lifecycle (browser):
//   1. Navigate the browser to {kratos}/self-service/{type}/browser
//      -> Kratos sets a flow cookie and 303-redirects to the configured
//         `ui_url` with `?flow=<id>`.
//   2. The UI page reads `?flow=<id>` and fetches the flow definition via
//      getFlow() to obtain `ui.nodes` / `ui.action` / `ui.method`.
//   3. The UI renders the form and submits it to `ui.action`.
// ---------------------------------------------------------------------------

import type {
  FlowType,
  KratosFlow,
  KratosLogoutFlow,
  KratosMessage,
  KratosSession,
} from './types';

/** Base URL of the Kratos public API, e.g. https://auth.myhoneydue.com */
export const KRATOS_URL =
  process.env.NEXT_PUBLIC_KRATOS_URL || 'https://auth.myhoneydue.com';

/** Always send credentials so flow + session cookies flow with the request. */
const CREDENTIALS: RequestCredentials = 'include';

/** Maps a flow type to its self-service flow path segment. */
const FLOW_PATH: Record<FlowType, string> = {
  login: 'login',
  registration: 'registration',
  recovery: 'recovery',
  verification: 'verification',
  settings: 'settings',
};

// ---------------------------------------------------------------------------
// Error
// ---------------------------------------------------------------------------

export class KratosError extends Error {
  constructor(
    public status: number,
    message: string,
    /** Parsed flow body if Kratos returned a 4xx with a fresh flow. */
    public flow?: KratosFlow,
  ) {
    super(message);
    this.name = 'KratosError';
  }
}

// ---------------------------------------------------------------------------
// Flow initialization
// ---------------------------------------------------------------------------

/**
 * Absolute URL that starts a browser self-service flow.
 * Navigating the browser here (full page load, NOT fetch) lets Kratos set the
 * flow cookie and redirect back to the configured `ui_url`.
 *
 * @param type      Flow category.
 * @param returnTo  Optional post-success redirect target (absolute URL).
 */
export function browserFlowUrl(type: FlowType, returnTo?: string): string {
  const url = new URL(`${KRATOS_URL}/self-service/${FLOW_PATH[type]}/browser`);
  if (returnTo) url.searchParams.set('return_to', returnTo);
  return url.toString();
}

/**
 * Starts a self-service flow by navigating the current browser window to
 * Kratos. This is a hard navigation — Kratos needs to set the flow cookie and
 * issue a redirect, which a `fetch` cannot do for a browser client.
 */
export function startFlow(type: FlowType, returnTo?: string): void {
  if (typeof window !== 'undefined') {
    window.location.href = browserFlowUrl(type, returnTo);
  }
}

// ---------------------------------------------------------------------------
// Flow fetching
// ---------------------------------------------------------------------------

/**
 * Fetches the definition of an already-initialized flow by id.
 * Called by the UI page after Kratos has redirected back with `?flow=<id>`.
 */
export async function getFlow(type: FlowType, id: string): Promise<KratosFlow> {
  const res = await fetch(
    `${KRATOS_URL}/self-service/${FLOW_PATH[type]}/flows?id=${encodeURIComponent(id)}`,
    {
      method: 'GET',
      credentials: CREDENTIALS,
      headers: { Accept: 'application/json' },
    },
  );

  if (!res.ok) {
    // 403/410 mean the flow expired or the cookie is missing — the caller
    // should re-initialize the flow.
    throw new KratosError(res.status, 'Flow expired or not found');
  }

  return (await res.json()) as KratosFlow;
}

// ---------------------------------------------------------------------------
// Session
// ---------------------------------------------------------------------------

/**
 * Returns the current Kratos session, or null if the browser is not logged in.
 * Used for route protection and to hydrate the user store.
 */
export async function whoami(): Promise<KratosSession | null> {
  try {
    const res = await fetch(`${KRATOS_URL}/sessions/whoami`, {
      method: 'GET',
      credentials: CREDENTIALS,
      headers: { Accept: 'application/json' },
    });
    if (res.status === 200) {
      return (await res.json()) as KratosSession;
    }
    // 401 = no/invalid session.
    return null;
  } catch {
    return null;
  }
}

/**
 * Initiates a browser logout flow and navigates the browser to the returned
 * `logout_url`, which clears the `ory_kratos_session` cookie. Kratos then
 * redirects to the configured `default_browser_return_url`.
 */
export async function logout(): Promise<void> {
  try {
    const res = await fetch(`${KRATOS_URL}/self-service/logout/browser`, {
      method: 'GET',
      credentials: CREDENTIALS,
      headers: { Accept: 'application/json' },
    });
    if (res.ok) {
      const data = (await res.json()) as KratosLogoutFlow;
      if (typeof window !== 'undefined' && data.logout_url) {
        window.location.href = data.logout_url;
        return;
      }
    }
  } catch {
    // fall through — best effort
  }
  // Fallback: if the logout flow could not be created (already logged out,
  // network error), just send the user to the login page.
  if (typeof window !== 'undefined') {
    window.location.href = '/login';
  }
}

// ---------------------------------------------------------------------------
// Form submission
// ---------------------------------------------------------------------------

/**
 * Submits a Kratos flow form to its `ui.action` URL.
 *
 * On success Kratos either:
 *   - returns 200 with a session payload (login / registration that completes
 *     immediately), or
 *   - returns a fresh flow body (e.g. recovery moved to the "code" step), or
 *   - returns 4xx with the same flow re-rendered with validation `messages`.
 *
 * Kratos browser flows redirect on hard success; with `Accept: application/json`
 * it instead returns JSON, which we hand back to the caller to inspect.
 *
 * @returns The parsed JSON body and the HTTP status.
 */
export async function submitFlow(
  action: string,
  method: string,
  body: Record<string, string>,
): Promise<{ status: number; ok: boolean; data: unknown }> {
  const res = await fetch(action, {
    method: method.toUpperCase() || 'POST',
    credentials: CREDENTIALS,
    headers: {
      'Content-Type': 'application/json',
      Accept: 'application/json',
    },
    body: JSON.stringify(body),
    // Kratos may 303 to the `ui_url`; we want the JSON body, not the redirect.
    redirect: 'manual',
  });

  // `redirect: 'manual'` surfaces a redirect as an opaque response (status 0).
  // For our flows Kratos returns JSON for both success and validation errors
  // when Accept: application/json is set, so a redirect here means a hard
  // success — treat it as such.
  if (res.status === 0 || res.type === 'opaqueredirect') {
    return { status: 200, ok: true, data: null };
  }

  const data = await res.json().catch(() => null);
  return { status: res.status, ok: res.ok, data };
}

// ---------------------------------------------------------------------------
// Message helpers
// ---------------------------------------------------------------------------

/** Collects all flow-level + node-level messages of the given types. */
export function collectMessages(
  flow: KratosFlow | null | undefined,
  types: string[] = ['error', 'info', 'success'],
): KratosMessage[] {
  if (!flow) return [];
  const out: KratosMessage[] = [];
  for (const m of flow.ui.messages ?? []) {
    if (types.includes(m.type)) out.push(m);
  }
  for (const node of flow.ui.nodes) {
    for (const m of node.messages ?? []) {
      if (types.includes(m.type)) out.push(m);
    }
  }
  return out;
}

/** First error message across the whole flow, or null. */
export function firstError(flow: KratosFlow | null | undefined): string | null {
  const errors = collectMessages(flow, ['error']);
  return errors.length > 0 ? errors[0].text : null;
}
