import {
  type APIRequestContext,
  type APIResponse,
  expect,
  test as setup,
} from "@playwright/test";
import {
  ADMIN_EMAIL,
  ADMIN_PASSWORD,
  DEFAULT_TEAM_NAME,
  EDITOR_EMAIL,
  ENGINEERING_TEAM_NAME,
  MARKETING_TEAM_NAME,
  MEMBER_EMAIL,
  UI_BASE_URL,
} from "./consts";

/**
 * Sleep for a given number of milliseconds
 */
function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

/**
 * Poll `/api/auth/get-session` until the backend serves 2xx. The endpoint
 * is public and returns 200 with a null user when unauthenticated, so a
 * non-2xx response signals the backend is still warming up (or down).
 */
async function waitForBackendReady(request: APIRequestContext): Promise<void> {
  const start = Date.now();
  // Capped well under the project's 60s per-test timeout so the rest of
  // the setup (signOut, signIn, teams creation, assignments) keeps headroom.
  const timeoutMs = 30_000;
  let delay = 500;
  let lastError: string | null = null;
  while (Date.now() - start < timeoutMs) {
    try {
      const response = await request.get(
        `${UI_BASE_URL}/api/auth/get-session`,
        { headers: { Origin: UI_BASE_URL } },
      );
      if (response.ok()) {
        return;
      }
      lastError = `${response.status()} ${await response.text()}`;
    } catch (err) {
      lastError = err instanceof Error ? err.message : String(err);
    }
    await sleep(delay);
    delay = Math.min(delay * 1.5, 5000);
  }
  throw new Error(
    `Backend did not respond OK to GET /api/auth/get-session within ${timeoutMs}ms (last: ${lastError ?? "n/a"})`,
  );
}

/**
 * Sign in a user via API and return true if successful
 * Handles rate limiting (429) with exponential backoff retry
 */
async function signInUser(
  request: APIRequestContext,
  email: string,
  password: string,
): Promise<boolean> {
  const response = await withRetries(
    () =>
      request.post(`${UI_BASE_URL}/api/auth/sign-in/email`, {
        data: { email, password },
        headers: {
          Origin: UI_BASE_URL,
        },
      }),
    { retryStatuses: [429, 500, 502, 503, 504] },
  );

  return response.ok();
}

interface OrgMember {
  id: string;
  userId: string;
  user: {
    id: string;
    email: string;
    name: string;
  };
}

/**
 * List organization members
 */
async function listOrgMembers(
  request: APIRequestContext,
): Promise<OrgMember[]> {
  const response = await request.get(
    `${UI_BASE_URL}/api/auth/organization/list-members`,
    {
      headers: { Origin: UI_BASE_URL },
    },
  );

  if (!response.ok()) {
    return [];
  }

  const data = await response.json();
  return data?.members ?? [];
}

interface Team {
  id: string;
  name: string;
  description: string | null;
}

function extractTeamsFromResponse(data: unknown): Team[] {
  if (Array.isArray(data)) {
    return data as Team[];
  }

  if (
    data &&
    typeof data === "object" &&
    "data" in data &&
    Array.isArray(data.data)
  ) {
    return data.data as Team[];
  }

  return [];
}

/**
 * Get all teams in the organization
 */
async function getTeams(request: APIRequestContext): Promise<Team[]> {
  const response = await request.get(`${UI_BASE_URL}/api/teams`, {
    headers: { Origin: UI_BASE_URL },
  });

  if (!response.ok()) {
    return [];
  }

  return extractTeamsFromResponse(await response.json());
}

function extractTeamFromResponse(data: unknown): Team | null {
  if (data && typeof data === "object" && "id" in data && "name" in data) {
    return data as Team;
  }

  if (
    data &&
    typeof data === "object" &&
    "data" in data &&
    data.data &&
    typeof data.data === "object" &&
    "id" in data.data &&
    "name" in data.data
  ) {
    return data.data as Team;
  }

  return null;
}

/**
 * Create a team if it doesn't already exist
 * Returns the team (existing or newly created)
 */
async function createTeamIfNotExists(
  request: APIRequestContext,
  name: string,
  description: string,
  existingTeams: Team[],
): Promise<Team> {
  const existing = existingTeams.find((t) => t.name === name);
  if (existing) {
    return existing;
  }

  const response = await withRetries(
    () =>
      request.post(`${UI_BASE_URL}/api/teams`, {
        data: { name, description },
        headers: {
          "Content-Type": "application/json",
          Origin: UI_BASE_URL,
        },
      }),
    { retryStatuses: [429, 500, 502, 503, 504] },
  );

  if (!response.ok()) {
    throw new Error(`Failed to create team ${name}: ${await response.text()}`);
  }

  const team = extractTeamFromResponse(await response.json());
  if (!team) {
    throw new Error(`Failed to parse created team ${name}`);
  }

  return team;
}

interface TeamMember {
  userId: string;
  role: string;
}

/**
 * Get members of a team
 */
async function getTeamMembers(
  request: APIRequestContext,
  teamId: string,
): Promise<TeamMember[]> {
  const response = await request.get(
    `${UI_BASE_URL}/api/teams/${teamId}/members`,
    {
      headers: { Origin: UI_BASE_URL },
    },
  );

  if (!response.ok()) {
    return [];
  }

  return response.json();
}

/**
 * Add a user to a team if not already a member
 */
async function addUserToTeamIfNotMember(
  request: APIRequestContext,
  teamId: string,
  userId: string,
  role: string,
  existingMembers: TeamMember[],
): Promise<void> {
  const isMember = existingMembers.some((m) => m.userId === userId);
  if (isMember) {
    return;
  }

  const response = await withRetries(
    () =>
      request.post(`${UI_BASE_URL}/api/teams/${teamId}/members`, {
        data: { userId, role },
        headers: {
          "Content-Type": "application/json",
          Origin: UI_BASE_URL,
        },
      }),
    { retryStatuses: [429, 500, 502, 503, 504] },
  );

  if (!response.ok()) {
    throw new Error(
      `Failed to add user ${userId} to team ${teamId}: ${await response.text()}`,
    );
  }
}

async function withRetries(
  requestFactory: () => Promise<APIResponse>,
  params: {
    retryStatuses: number[];
    maxRetries?: number;
    initialDelayMs?: number;
  },
) {
  const maxRetries = params.maxRetries ?? 3;
  let delay = params.initialDelayMs ?? 1000;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await requestFactory();
    if (response.ok()) {
      return response;
    }

    if (
      attempt < maxRetries &&
      params.retryStatuses.includes(response.status())
    ) {
      // better-auth's 429 carries x-retry-after (seconds until its rate-limit
      // window clears). Honor it so a transient rate-limit recovers within
      // this setup attempt instead of forcing a full test-level retry — the
      // default exponential backoff never sleeps longer than the ~10s window,
      // and retrying inside the window keeps the limiter's bucket full.
      const headers = response.headers();
      const retryAfterSec = Number.parseInt(
        headers["x-retry-after"] ?? headers["retry-after"] ?? "",
        10,
      );
      const waitMs = Number.isFinite(retryAfterSec)
        ? Math.min(retryAfterSec * 1000 + 500, 20_000)
        : delay;
      await sleep(waitMs);
      delay *= 2;
      continue;
    }

    return response;
  }

  throw new Error("Retry loop exited unexpectedly");
}

async function signOut(request: APIRequestContext): Promise<void> {
  await request.post(`${UI_BASE_URL}/api/auth/sign-out`, {
    headers: { Origin: UI_BASE_URL },
  });
}

async function getSessionUserEmail(
  request: APIRequestContext,
): Promise<string | null> {
  const response = await request.get(`${UI_BASE_URL}/api/auth/get-session`, {
    headers: { Origin: UI_BASE_URL },
  });
  if (!response.ok()) {
    return null;
  }

  const data = await response.json();
  return data?.user?.email ?? null;
}

async function assertAdminPermissions(
  request: APIRequestContext,
): Promise<void> {
  const response = await request.get(`${UI_BASE_URL}/api/user/permissions`, {
    headers: { Origin: UI_BASE_URL },
  });
  expect(response.ok(), "Failed to fetch admin permissions").toBe(true);

  const permissions = await response.json();
  expect(
    permissions?.identityProvider,
    "Teams setup session is not using the admin role",
  ).toContain("create");
  expect(
    permissions?.mcpRegistry,
    "Teams setup session is not using the admin role",
  ).toContain("create");
}

// Setup teams - runs after users are created
setup("setup teams and assignments", async ({ page }) => {
  // Single up-front backend readiness probe; the signOut→signIn→GET chain
  // below has no warm-up tolerance, so transient connection / 5xx during
  // pod warmup intermittently aborted the chain mid-flight.
  await waitForBackendReady(page.request);

  await signOut(page.request);
  await page.context().clearCookies();

  // Sign in as admin
  const signedIn = await signInUser(page.request, ADMIN_EMAIL, ADMIN_PASSWORD);
  expect(signedIn, "Admin sign-in failed for teams setup").toBe(true);

  // Navigate to establish cookie context
  await page.goto(`${UI_BASE_URL}/chat`);
  await page.waitForLoadState("domcontentloaded");
  const sessionEmail = await getSessionUserEmail(page.request);
  expect(sessionEmail, "Teams setup did not save an admin session").toBe(
    ADMIN_EMAIL,
  );
  await assertAdminPermissions(page.request);

  // Get organization members to find editor and member user IDs
  const members = await listOrgMembers(page.request);

  const adminMember = members.find((m) => m.user.email === ADMIN_EMAIL);
  const editorMember = members.find((m) => m.user.email === EDITOR_EMAIL);
  const memberMember = members.find((m) => m.user.email === MEMBER_EMAIL);

  expect(adminMember, `Admin user ${ADMIN_EMAIL} not found`).toBeTruthy();
  expect(editorMember, `Editor user ${EDITOR_EMAIL} not found`).toBeTruthy();
  expect(memberMember, `Member user ${MEMBER_EMAIL} not found`).toBeTruthy();

  const adminUserId = adminMember?.userId ?? "";
  const editorUserId = editorMember?.userId ?? "";
  const memberUserId = memberMember?.userId ?? "";

  // Get existing teams
  const existingTeams = await getTeams(page.request);

  // Create Default Team if not exists (no longer auto-created by seed)
  const defaultTeam = await createTeamIfNotExists(
    page.request,
    DEFAULT_TEAM_NAME,
    "Default team for e2e tests",
    existingTeams,
  );

  // Create Engineering Team if not exists
  const engineeringTeam = await createTeamIfNotExists(
    page.request,
    ENGINEERING_TEAM_NAME,
    "Engineering team for e2e tests",
    existingTeams,
  );

  // Create Marketing Team if not exists
  const marketingTeam = await createTeamIfNotExists(
    page.request,
    MARKETING_TEAM_NAME,
    "Marketing team for e2e tests",
    existingTeams,
  );

  // Get team members
  const defaultMembers = await getTeamMembers(page.request, defaultTeam.id);
  const engineeringMembers = await getTeamMembers(
    page.request,
    engineeringTeam.id,
  );
  const marketingMembers = await getTeamMembers(page.request, marketingTeam.id);

  // Add all users to Default Team (previously handled by seed auto-assignment)
  await addUserToTeamIfNotMember(
    page.request,
    defaultTeam.id,
    adminUserId,
    "member",
    defaultMembers,
  );
  await addUserToTeamIfNotMember(
    page.request,
    defaultTeam.id,
    editorUserId,
    "member",
    defaultMembers,
  );
  await addUserToTeamIfNotMember(
    page.request,
    defaultTeam.id,
    memberUserId,
    "member",
    defaultMembers,
  );

  // Add Admin and Editor to both Engineering and Marketing teams
  await addUserToTeamIfNotMember(
    page.request,
    engineeringTeam.id,
    adminUserId,
    "member",
    engineeringMembers,
  );
  await addUserToTeamIfNotMember(
    page.request,
    engineeringTeam.id,
    editorUserId,
    "member",
    engineeringMembers,
  );
  await addUserToTeamIfNotMember(
    page.request,
    marketingTeam.id,
    adminUserId,
    "member",
    marketingMembers,
  );
  await addUserToTeamIfNotMember(
    page.request,
    marketingTeam.id,
    editorUserId,
    "member",
    marketingMembers,
  );

  // Add Member only to Marketing Team
  await addUserToTeamIfNotMember(
    page.request,
    marketingTeam.id,
    memberUserId,
    "member",
    marketingMembers,
  );

  await signOut(page.request);
});
