import type {
  CustodyWalletMetadataResponse,
  ListApiKeysResponse,
  PolicyDecision,
  WalletControlProfileRevisionHistory,
  WalletOperationFamily,
  WalletOperationStatus,
  WalletPolicyEvaluationDetail,
} from "@sdp/types";
import { resolveMemberIdentity } from "@/app/dashboard/settings/member-identity";
import type { Member } from "@/app/members/actions";
import type { SdpApiClient } from "@/lib/sdp-api";
import { getWalletMetadataPath } from "@/lib/sdp-api-paths";

export const POLICY_AUDIT_PAGE_SIZE = 25;
const POLICY_AUDIT_PAGE_SIZES: readonly number[] = [10, 25, 50, 100];
const POLICY_AUDIT_API_PAGE_SIZE = 100;
const POLICY_AUDIT_MAX_LOCAL_FILTER_PAGES = 50;

const POLICY_DECISIONS = [
  "allow",
  "deny",
  "approval_required",
  "provider_approval_required",
  "review",
  "not_evaluated",
] as const satisfies readonly PolicyDecision[];

export const POLICY_AUDIT_OPERATION_STATUSES = [
  "created",
  "evaluated",
  "pending_approval",
  "executing",
  "completed",
  "failed",
  "canceled",
] as const satisfies readonly WalletOperationStatus[];

export const POLICY_AUDIT_OPERATION_FAMILIES = [
  "transfer",
  "payment",
  "ramp",
  "issuance",
  "raw_sign",
  "program",
  "provider_admin",
] as const satisfies readonly WalletOperationFamily[];

export interface PolicyAuditFilters {
  page: number;
  pageSize: number;
  decision?: PolicyDecision;
  status?: WalletOperationStatus;
  operationFamily?: WalletOperationFamily;
  reasonCode?: string;
  from?: string;
  to?: string;
}

export interface PolicyAuditListResult {
  evaluations: WalletPolicyEvaluationDetail[];
  total: number;
  page: number;
  pageSize: number;
}

export interface PolicyAuditContext {
  wallet: CustodyWalletMetadataResponse["wallet"];
  revisionHistory: WalletControlProfileRevisionHistory;
  apiKeyNames: Record<string, string>;
  userNames: Record<string, string>;
}

export type PolicyRevisionContext = Pick<PolicyAuditContext, "wallet" | "revisionHistory">;

export interface PolicyAuditNeighbor {
  id: string;
  page: number;
}

export interface PolicyAuditNeighbors {
  previous: PolicyAuditNeighbor | null;
  next: PolicyAuditNeighbor | null;
}

type SearchParams = Record<string, string | string[] | undefined>;
type AuditApiMeta = {
  total: number;
  page: number;
  pageSize: number;
  hasMore: boolean;
};

interface AuditApiPage {
  data: WalletPolicyEvaluationDetail[];
  meta: AuditApiMeta;
}

export class PolicyAuditRequestError extends Error {
  constructor(
    message: string,
    readonly status: number
  ) {
    super(message);
    this.name = "PolicyAuditRequestError";
  }
}

export function firstSearchParam(value: string | string[] | undefined): string | undefined {
  return Array.isArray(value) ? value[0] : value;
}

function enumValue<T extends string>(
  value: string | undefined,
  values: readonly T[]
): T | undefined {
  return value && values.includes(value as T) ? (value as T) : undefined;
}

function dateValue(value: string | undefined): string | undefined {
  if (!value || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return undefined;
  const parsed = new Date(`${value}T00:00:00.000Z`);
  return Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== value
    ? undefined
    : value;
}

export function parsePolicyAuditFilters(searchParams: SearchParams): PolicyAuditFilters {
  const parsedPage = Number.parseInt(firstSearchParam(searchParams.page) ?? "1", 10);
  const parsedPageSize = Number.parseInt(firstSearchParam(searchParams.pageSize) ?? "", 10);
  return {
    page: Number.isInteger(parsedPage) && parsedPage > 0 ? parsedPage : 1,
    pageSize: POLICY_AUDIT_PAGE_SIZES.includes(parsedPageSize)
      ? parsedPageSize
      : POLICY_AUDIT_PAGE_SIZE,
    decision: enumValue(firstSearchParam(searchParams.decision), POLICY_DECISIONS),
    status: enumValue(firstSearchParam(searchParams.status), POLICY_AUDIT_OPERATION_STATUSES),
    operationFamily: enumValue(
      firstSearchParam(searchParams.operationFamily),
      POLICY_AUDIT_OPERATION_FAMILIES
    ),
    reasonCode: firstSearchParam(searchParams.reasonCode)?.trim().slice(0, 100) || undefined,
    from: dateValue(firstSearchParam(searchParams.from)),
    to: dateValue(firstSearchParam(searchParams.to)),
  };
}

export function hasPolicyAuditFilters(filters: PolicyAuditFilters): boolean {
  return Boolean(
    filters.decision ||
      filters.status ||
      filters.operationFamily ||
      filters.reasonCode ||
      filters.from ||
      filters.to
  );
}

export function buildPolicyAuditSearchParams(
  filters: PolicyAuditFilters,
  overrides: Partial<PolicyAuditFilters> = {}
): URLSearchParams {
  const values = { ...filters, ...overrides };
  const query = new URLSearchParams();
  if (values.page > 1) query.set("page", String(values.page));
  if (values.pageSize !== POLICY_AUDIT_PAGE_SIZE) query.set("pageSize", String(values.pageSize));
  if (values.decision) query.set("decision", values.decision);
  if (values.status) query.set("status", values.status);
  if (values.operationFamily) query.set("operationFamily", values.operationFamily);
  if (values.reasonCode) query.set("reasonCode", values.reasonCode);
  if (values.from) query.set("from", values.from);
  if (values.to) query.set("to", values.to);
  return query;
}

function buildPolicyAuditApiQuery(
  filters: Omit<PolicyAuditFilters, "from" | "to">,
  page: number,
  pageSize: number,
  decision = filters.decision
): URLSearchParams {
  const query = new URLSearchParams({ page: String(page), pageSize: String(pageSize) });
  if (decision) query.set("decision", decision);
  if (filters.status) query.set("status", filters.status);
  if (filters.operationFamily) query.set("operationFamily", filters.operationFamily);
  if (filters.reasonCode) query.set("reasonCode", filters.reasonCode);
  return query;
}

async function readError(response: Response): Promise<string> {
  const body = (await response.json().catch(() => null)) as {
    error?: { message?: string } | string;
    message?: string;
  } | null;
  if (typeof body?.error === "string") return body.error;
  return body?.error?.message ?? body?.message ?? `Request failed (${response.status})`;
}

async function requestAuditPage(
  request: SdpApiClient["request"],
  walletId: string,
  filters: PolicyAuditFilters,
  page: number,
  pageSize: number,
  decision = filters.decision
): Promise<AuditApiPage> {
  const query = buildPolicyAuditApiQuery(filters, page, pageSize, decision);
  const response = await request(
    `/v1/payments/wallets/${encodeURIComponent(walletId)}/policies/evaluations?${query}`
  );
  if (!response.ok) {
    throw new PolicyAuditRequestError(await readError(response), response.status);
  }

  const body = (await response.json()) as Partial<AuditApiPage>;
  return {
    data: body.data ?? [],
    meta: body.meta ?? {
      total: body.data?.length ?? 0,
      page,
      pageSize,
      hasMore: false,
    },
  };
}

function evaluationIsInDateRange(
  evaluation: WalletPolicyEvaluationDetail,
  filters: Pick<PolicyAuditFilters, "from" | "to">
): boolean {
  const timestamp = Date.parse(evaluation.evaluatedAt);
  if (Number.isNaN(timestamp)) return false;
  if (filters.from && timestamp < Date.parse(`${filters.from}T00:00:00.000Z`)) return false;
  if (filters.to && timestamp > Date.parse(`${filters.to}T23:59:59.999Z`)) return false;
  return true;
}

async function collectPolicyEvaluations(
  request: SdpApiClient["request"],
  walletId: string,
  filters: PolicyAuditFilters,
  decision: PolicyDecision | undefined
): Promise<WalletPolicyEvaluationDetail[]> {
  const evaluations: WalletPolicyEvaluationDetail[] = [];
  let page = 1;

  while (true) {
    const result = await requestAuditPage(
      request,
      walletId,
      filters,
      page,
      POLICY_AUDIT_API_PAGE_SIZE,
      decision
    );
    evaluations.push(...result.data.filter((item) => evaluationIsInDateRange(item, filters)));

    const fromTimestamp = filters.from
      ? Date.parse(`${filters.from}T00:00:00.000Z`)
      : Number.NEGATIVE_INFINITY;
    const reachedLowerBound = result.data.some(
      (item) => Date.parse(item.evaluatedAt) < fromTimestamp
    );
    const pageCount = Math.max(1, Math.ceil(result.meta.total / Math.max(1, result.meta.pageSize)));
    if (!result.meta.hasMore || reachedLowerBound || page >= pageCount) break;
    if (page >= POLICY_AUDIT_MAX_LOCAL_FILTER_PAGES) {
      throw new PolicyAuditRequestError(
        "Policy audit history exceeds the local filtering limit",
        422
      );
    }
    page += 1;
  }

  return evaluations;
}

function needsLocalPolicyAuditPagination(filters: PolicyAuditFilters): boolean {
  return Boolean(filters.from || filters.to || filters.decision === "approval_required");
}

async function collectLocallyPaginatedPolicyEvaluations(
  request: SdpApiClient["request"],
  walletId: string,
  filters: PolicyAuditFilters
): Promise<WalletPolicyEvaluationDetail[]> {
  const decisions: Array<PolicyDecision | undefined> =
    filters.decision === "approval_required"
      ? ["approval_required", "provider_approval_required"]
      : [filters.decision];
  const pages = await Promise.all(
    decisions.map((decision) => collectPolicyEvaluations(request, walletId, filters, decision))
  );
  return pages
    .flat()
    .sort((left, right) => Date.parse(right.evaluatedAt) - Date.parse(left.evaluatedAt));
}

export async function fetchPolicyAuditList(
  request: SdpApiClient["request"],
  walletId: string,
  filters: PolicyAuditFilters
): Promise<PolicyAuditListResult> {
  if (!needsLocalPolicyAuditPagination(filters)) {
    const result = await requestAuditPage(
      request,
      walletId,
      filters,
      filters.page,
      filters.pageSize
    );
    return {
      evaluations: result.data,
      total: result.meta.total,
      page: filters.page,
      pageSize: filters.pageSize,
    };
  }

  const evaluations = await collectLocallyPaginatedPolicyEvaluations(request, walletId, filters);
  const pageCount = Math.max(1, Math.ceil(evaluations.length / filters.pageSize));
  const page = Math.min(filters.page, pageCount);
  const start = (page - 1) * filters.pageSize;

  return {
    evaluations: evaluations.slice(start, start + filters.pageSize),
    total: evaluations.length,
    page,
    pageSize: filters.pageSize,
  };
}

async function fetchWallet(
  request: SdpApiClient["request"],
  walletId: string
): Promise<CustodyWalletMetadataResponse["wallet"]> {
  const response = await request(getWalletMetadataPath(walletId));
  if (!response.ok) {
    throw new PolicyAuditRequestError(await readError(response), response.status);
  }
  const body = (await response.json()) as { data?: CustodyWalletMetadataResponse };
  if (!body.data?.wallet) {
    throw new PolicyAuditRequestError("Wallet not found", 404);
  }
  return body.data.wallet;
}

export async function fetchRevisionHistory(
  request: SdpApiClient["request"],
  walletId: string
): Promise<WalletControlProfileRevisionHistory> {
  const response = await request(
    `/v1/payments/wallets/${encodeURIComponent(walletId)}/policies/revisions`
  );
  if (!response.ok) {
    throw new PolicyAuditRequestError(await readError(response), response.status);
  }
  const body = (await response.json()) as { data?: WalletControlProfileRevisionHistory };
  return body.data ?? { profile: null, revisions: [] };
}

async function fetchApiKeyNames(request: SdpApiClient["request"]): Promise<Record<string, string>> {
  try {
    const response = await request("/v1/api-keys");
    if (!response.ok) return {};
    const body = (await response.json()) as { data?: ListApiKeysResponse };
    return Object.fromEntries((body.data?.apiKeys ?? []).map((key) => [key.id, key.name]));
  } catch {
    return {};
  }
}

/**
 * Maps organization member user ids to a human label via the canonical
 * {@link resolveMemberIdentity} chain, so audit actors read as people instead
 * of `usr_…` ids. Unresolved members are omitted rather than given a
 * placeholder — like API key names, the labels are decorative and a miss
 * falls back to the raw id in the UI. Fetches one 100-member page
 * (ponytail: ids beyond it render unresolved).
 *
 * @param request - Authenticated SDP API request function.
 * @returns Map of user id to display label; empty when the directory is unavailable.
 */
export async function fetchMemberNames(
  request: SdpApiClient["request"]
): Promise<Record<string, string>> {
  try {
    const response = await request("/v1/members?pageSize=100");
    if (!response.ok) return {};
    const body = (await response.json()) as { data?: { members?: Member[] } };
    return Object.fromEntries(
      (body.data?.members ?? []).flatMap((member) => {
        const identity = resolveMemberIdentity(member.user, "");
        return identity.isUnresolved ? [] : [[member.user.id, identity.label]];
      })
    );
  } catch {
    return {};
  }
}

export async function fetchPolicyAuditContext(
  request: SdpApiClient["request"],
  walletId: string
): Promise<PolicyAuditContext> {
  const [revisionContext, apiKeyNames, userNames] = await Promise.all([
    fetchPolicyRevisionContext(request, walletId),
    fetchApiKeyNames(request),
    fetchMemberNames(request),
  ]);
  return { ...revisionContext, apiKeyNames, userNames };
}

export async function fetchPolicyRevisionContext(
  request: SdpApiClient["request"],
  walletId: string
): Promise<PolicyRevisionContext> {
  const [wallet, revisionHistory] = await Promise.all([
    fetchWallet(request, walletId),
    fetchRevisionHistory(request, walletId),
  ]);
  return { wallet, revisionHistory };
}

export async function fetchPolicyEvaluation(
  request: SdpApiClient["request"],
  walletId: string,
  policyEvaluationId: string
): Promise<WalletPolicyEvaluationDetail> {
  const response = await request(
    `/v1/payments/wallets/${encodeURIComponent(walletId)}/policies/evaluations/${encodeURIComponent(policyEvaluationId)}`
  );
  if (!response.ok) {
    throw new PolicyAuditRequestError(await readError(response), response.status);
  }
  const body = (await response.json()) as {
    data?: { policyEvaluation?: WalletPolicyEvaluationDetail };
  };
  if (!body.data?.policyEvaluation) {
    throw new PolicyAuditRequestError("Policy evaluation not found", 404);
  }
  return body.data.policyEvaluation;
}

export async function fetchPolicyEvaluationNeighbors(
  request: SdpApiClient["request"],
  walletId: string,
  policyEvaluationId: string,
  filters: PolicyAuditFilters
): Promise<PolicyAuditNeighbors> {
  if (needsLocalPolicyAuditPagination(filters)) {
    const evaluations = await collectLocallyPaginatedPolicyEvaluations(request, walletId, filters);
    const index = evaluations.findIndex((item) => item.id === policyEvaluationId);
    if (index === -1) return { previous: null, next: null };

    const previousIndex = index - 1;
    const nextIndex = index + 1;
    return {
      previous:
        previousIndex >= 0
          ? {
              id: evaluations[previousIndex].id,
              page: Math.floor(previousIndex / filters.pageSize) + 1,
            }
          : null,
      next:
        nextIndex < evaluations.length
          ? {
              id: evaluations[nextIndex].id,
              page: Math.floor(nextIndex / filters.pageSize) + 1,
            }
          : null,
    };
  }

  const current = await fetchPolicyAuditList(request, walletId, filters);
  const index = current.evaluations.findIndex((item) => item.id === policyEvaluationId);
  if (index === -1) return { previous: null, next: null };

  let previous = index > 0 ? { id: current.evaluations[index - 1].id, page: current.page } : null;
  let next =
    index < current.evaluations.length - 1
      ? { id: current.evaluations[index + 1].id, page: current.page }
      : null;

  if (!previous && current.page > 1) {
    const page = current.page - 1;
    const adjacent = await fetchPolicyAuditList(request, walletId, { ...filters, page });
    const evaluation = adjacent.evaluations.at(-1);
    previous = evaluation ? { id: evaluation.id, page } : null;
  }
  if (!next && current.page * current.pageSize < current.total) {
    const page = current.page + 1;
    const adjacent = await fetchPolicyAuditList(request, walletId, { ...filters, page });
    const evaluation = adjacent.evaluations[0];
    next = evaluation ? { id: evaluation.id, page } : null;
  }

  return { previous, next };
}
