import { ForbiddenError, NotFoundError } from '@server/errors';

export interface BranchAwareSession {
  role?: string;
  pinnedBranchId?: string | null;
  branchId?: string | null;
}

const FORBIDDEN_BRANCH_MSG = 'You can only access data for your assigned branch.';

export function isBranchPinned(session: BranchAwareSession): boolean {
  return !!session.pinnedBranchId;
}

export function effectiveBranchId(session: BranchAwareSession): string | null {
  return session.pinnedBranchId ?? session.branchId ?? null;
}

/** 403 if a hard-pinned staff user accesses a sibling branch's resource. */
export function assertBranchAccess(
  session: BranchAwareSession,
  resourceBranchId: string | null | undefined,
  opts: { allowNull?: boolean } = {},
): void {
  const pinned = session.pinnedBranchId ?? null;
  if (!pinned) return;
  if (resourceBranchId == null) {
    if (opts.allowNull) return;
    throw new ForbiddenError(FORBIDDEN_BRANCH_MSG);
  }
  if (resourceBranchId !== pinned) throw new ForbiddenError(FORBIDDEN_BRANCH_MSG);
}

/** 403 unless the caller is the restaurant owner. */
export function requireOwner(session: BranchAwareSession, action = 'perform this action'): void {
  if (session.role !== 'owner') {
    throw new ForbiddenError(`Only restaurant owners can ${action}.`);
  }
}

/** Resolve a caller-supplied branch_id for create/update flows. */
export function resolveWritableBranchId(
  session: BranchAwareSession,
  supplied: string | null | undefined,
): string | null {
  const pinned = session.pinnedBranchId ?? null;
  if (pinned) {
    if (supplied == null) return pinned;
    if (supplied !== pinned) {
      throw new ForbiddenError('You can only create or edit resources for your assigned branch.');
    }
    return pinned;
  }
  return supplied ?? null;
}

/**
 * Validate a marketing-rules `branchIds` array against the caller's scope.
 * Pinned/active-branch callers may only target their own branch; empty/undefined
 * collapses to `[scope]` so the audience query never widens to all branches.
 */
export function assertRulesBranchScope(
  session: BranchAwareSession,
  branchIds: string[] | null | undefined,
): string[] | undefined {
  const scope = effectiveBranchId(session);
  if (!scope) return Array.isArray(branchIds) ? branchIds : undefined;
  if (!Array.isArray(branchIds) || branchIds.length === 0) return [scope];
  for (const id of branchIds) {
    if (id !== scope) {
      throw new ForbiddenError(
        'You can only target your assigned branch in marketing audience rules.',
      );
    }
  }
  return branchIds;
}

/**
 * 403-vs-404 distinction for marketing detail/mutation routes. `loadUnscoped`
 * runs only on the empty-scoped path; if it finds the row, the caller is
 * forbidden; otherwise it genuinely doesn't exist.
 */
export async function loadAccessibleOrThrow<T>(
  scoped: T | null,
  loadUnscoped: () => Promise<T | null>,
  resourceLabel: string,
): Promise<T> {
  if (scoped) return scoped;
  const unscoped = await loadUnscoped();
  if (unscoped) throw new ForbiddenError(FORBIDDEN_BRANCH_MSG);
  throw new NotFoundError(resourceLabel);
}

/** Same semantics as `assertRulesBranchScope` but on the parsed rules object. */
export function scopeAudienceRules<T extends { branchIds?: string[] | null }>(
  session: BranchAwareSession,
  rules: T | undefined | null,
): T {
  const r = (rules ?? {}) as T;
  const cleaned = assertRulesBranchScope(session, r.branchIds ?? undefined);
  if (cleaned === undefined) {
    const { branchIds: _drop, ...rest } = r as T & { branchIds?: unknown };
    return rest as T;
  }
  return { ...r, branchIds: cleaned } as T;
}
