import { ForbiddenException } from '@nestjs/common';
import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type';

import { FREIGHT_PERMS } from '../seed/freight-permissions.registry';

const SUPER_ADMIN_ROLE = 'super_admin';
const ORGANIZATION_ADMIN_ROLE = 'organization_admin';

type PermissionLike = { key?: string };
type PositionTypeLike = { key?: string };
type MeLikeUser = {
  roles?: { key?: string }[];
  permissions?: PermissionLike[];
  employee?:
    | {
        position?: {
          permissions?: PermissionLike[];
          positionType?: PositionTypeLike | null;
        };
        delegatedPositions?: { permissions?: PermissionLike[] }[];
      }
    | {
        positions?: {
          permissions?: PermissionLike[];
          positionType?: PositionTypeLike | null;
        }[];
      }[]
    | null;
};

export function isSuperAdmin(user: MeLikeUser | null | undefined): boolean {
  if (!user?.roles?.length) return false;
  return user.roles.some((r) => r.key === SUPER_ADMIN_ROLE);
}

export function isOrganizationAdmin(user: MeLikeUser | null | undefined): boolean {
  if (!user?.roles?.length) return false;
  return user.roles.some((r) => r.key === ORGANIZATION_ADMIN_ROLE);
}

export function isFreightApprovalAdmin(user: MeLikeUser | null | undefined): boolean {
  return isSuperAdmin(user) || isOrganizationAdmin(user);
}

/** Flat permission keys from JWT / session user (roles + position permissions). */
export function collectPermissionKeys(user: MeLikeUser | null | undefined): string[] {
  if (!user) return [];

  const keys = new Set<string>();

  for (const p of user.permissions ?? []) {
    if (p.key) keys.add(p.key);
  }

  const employee = user.employee;
  if (!employee) {
    return [...keys];
  }

  if (Array.isArray(employee)) {
    for (const emp of employee) {
      for (const pos of emp.positions ?? []) {
        for (const p of pos.permissions ?? []) {
          if (p.key) keys.add(p.key);
        }
      }
    }
    return [...keys];
  }

  for (const p of employee.position?.permissions ?? []) {
    if (p.key) keys.add(p.key);
  }
  for (const delegated of employee.delegatedPositions ?? []) {
    for (const p of delegated.permissions ?? []) {
      if (p.key) keys.add(p.key);
    }
  }

  return [...keys];
}

export function hasFreightPermission(
  user: MeLikeUser | null | undefined,
  permissionKey: string,
): boolean {
  if (!user) return false;
  if (isSuperAdmin(user)) return true;
  return collectPermissionKeys(user).includes(permissionKey);
}

export function assertFreightPermission(
  user: TCurrentUser | MeLikeUser | null | undefined,
  permissionKey: string,
): void {
  if (hasFreightPermission(user, permissionKey)) return;
  throw new ForbiddenException(`Missing permission: ${permissionKey}`);
}

/**
 * The caller's IAM position-type keys (`iam.position_types.key`). A position
 * type is the platform's notion of a role — it is what carries permissions via
 * `iam.position_type_permissions` — and it is the vocabulary contract approval
 * chains are configured in.
 *
 * Mirrors `collectPermissionKeys`' handling of both JWT shapes: `employee` is
 * an object on some tokens and an array on others.
 *
 * Note delegated positions carry no `positionType` in the token, so a delegate
 * is not reachable here — they authorize through the permission arm of
 * `assertCanApproveContractStep` instead.
 */
export function collectPositionTypeKeys(
  user: MeLikeUser | null | undefined,
): string[] {
  const employee = user?.employee;
  if (!employee) return [];

  const keys = new Set<string>();

  if (Array.isArray(employee)) {
    for (const emp of employee) {
      for (const pos of emp.positions ?? []) {
        if (pos.positionType?.key) keys.add(pos.positionType.key);
      }
    }
    return [...keys];
  }

  if (employee.position?.positionType?.key) {
    keys.add(employee.position.positionType.key);
  }
  return [...keys];
}

/**
 * Legacy chain roles predate position types. Historical `approval_rules` and
 * in-flight `contract_approval_steps` rows still carry them, so map each to the
 * position types that stand in for it. Without this, an approver holding a
 * modern position type could not action an older step.
 */
const LEGACY_ROLE_POSITION_TYPES: Record<string, string[]> = {
  LINE_STAFF: ['employee', 'teamLeader', 'officeHead', 'recordOfficer'],
  DIRECTOR: ['director', 'operation-director'],
  CEO: ['chief', 'deputy'],
};

const APPROVE_ROLE_PERMISSION: Record<string, string> = {
  LINE_STAFF: FREIGHT_PERMS.bookings.approveLineStaff,
  DIRECTOR: FREIGHT_PERMS.bookings.approveDirector,
  CEO: FREIGHT_PERMS.bookings.approveCeo,
};

/**
 * Approval-chain roles synthesized for hazardous contracts (see
 * `instantiateApprovalSteps`). Unlike the legacy roles below they are NOT
 * position types — they authorize purely on their own dedicated permission, and
 * they deliberately opt out of the blanket "holds any contract-approve
 * permission" fallback so a normal approver cannot sign off dangerous goods.
 */
export const HAZARDOUS_APPROVAL_ROLE_PERMISSION: Record<string, string> = {
  HAZARDOUS_APPROVAL_ONE: FREIGHT_PERMS.contracts.hazardousApprovalOne,
  HAZARDOUS_APPROVAL_TWO: FREIGHT_PERMS.contracts.hazardousApprovalTwo,
};

/** The two hazardous steps, in the order they are prepended to the chain. */
export const HAZARDOUS_APPROVAL_ROLES = [
  'HAZARDOUS_APPROVAL_ONE',
  'HAZARDOUS_APPROVAL_TWO',
] as const;

const CONTRACT_APPROVE_ROLE_PERMISSION: Record<string, string> = {
  LINE_STAFF: FREIGHT_PERMS.contracts.approveLineStaff,
  DIRECTOR: FREIGHT_PERMS.contracts.approveDirector,
  CEO: FREIGHT_PERMS.contracts.approveCeo,
};

const ANY_CONTRACT_APPROVE_PERMISSION = [
  FREIGHT_PERMS.contracts.approveLineStaff,
  FREIGHT_PERMS.contracts.approveDirector,
  FREIGHT_PERMS.contracts.approveCeo,
];

/**
 * May this caller action a contract approval step requiring `requiredRole`?
 *
 * `requiredRole` is an `iam.position_types.key` for chains configured by an
 * admin, or one of the legacy LINE_STAFF/DIRECTOR/CEO strings for older rows.
 * A caller passes when any of these hold:
 *
 *  - they are a super/organization admin (blanket bypass);
 *  - their position type matches the step, directly or via a legacy alias;
 *  - they hold the approve permission the legacy role maps to;
 *  - they hold any contract approve permission — this covers delegates (whose
 *    position type is absent from the token) and staff whose IAM position has
 *    no position type assigned yet.
 */
export function assertCanApproveContractStep(
  user: TCurrentUser | MeLikeUser | null | undefined,
  requiredRole: string,
): void {
  if (isFreightApprovalAdmin(user)) return;

  // Hazardous steps are permission-only and strict — no legacy alias, no
  // blanket approve fallback.
  const hazardousPermission = HAZARDOUS_APPROVAL_ROLE_PERMISSION[requiredRole];
  if (hazardousPermission) {
    if (hasFreightPermission(user, hazardousPermission)) return;
    throw new ForbiddenException(
      `Missing permission: ${hazardousPermission}`,
    );
  }

  const positionTypes = collectPositionTypeKeys(user);
  if (positionTypes.includes(requiredRole)) return;

  const aliases = LEGACY_ROLE_POSITION_TYPES[requiredRole] ?? [];
  if (aliases.some((alias) => positionTypes.includes(alias))) return;

  const legacyPermission = CONTRACT_APPROVE_ROLE_PERMISSION[requiredRole];
  if (legacyPermission && hasFreightPermission(user, legacyPermission)) return;

  if (ANY_CONTRACT_APPROVE_PERMISSION.some((p) => hasFreightPermission(user, p))) {
    return;
  }

  throw new ForbiddenException(
    `You are not the required approver (${requiredRole}) for this step.`,
  );
}

/**
 * Strict "is it exactly this caller's turn?" test — mirrors the backoffice
 * `canApproveContractStep`. Same passes as {@link assertCanApproveContractStep}
 * EXCEPT the blanket "holds any contract-approve permission" fallback is
 * dropped: a line-staff holding `approveLineStaff` must NOT read as the director
 * for a director step. Used to gate contract-document editing so approval hands
 * edit rights to the NEXT approver only — a previous approver who already acted
 * (but still holds an approve permission) loses the edit button, as required.
 *
 * (Kept separate from the approve/reject gate, which keeps the blanket fallback
 * so delegates whose token omits a position type can still action their step.)
 */
export function canEditContractStep(
  user: TCurrentUser | MeLikeUser | null | undefined,
  requiredRole: string,
): boolean {
  if (isFreightApprovalAdmin(user)) return true;

  const hazardousPermission = HAZARDOUS_APPROVAL_ROLE_PERMISSION[requiredRole];
  if (hazardousPermission) {
    return hasFreightPermission(user, hazardousPermission);
  }

  const positionTypes = collectPositionTypeKeys(user);
  if (positionTypes.includes(requiredRole)) return true;

  const aliases = LEGACY_ROLE_POSITION_TYPES[requiredRole] ?? [];
  if (aliases.some((alias) => positionTypes.includes(alias))) return true;

  const legacyPermission = CONTRACT_APPROVE_ROLE_PERMISSION[requiredRole];
  return Boolean(legacyPermission && hasFreightPermission(user, legacyPermission));
}

export function assertCanApproveBookingStep(
  user: TCurrentUser | MeLikeUser | null | undefined,
  requiredRole: string,
): void {
  if (isFreightApprovalAdmin(user)) return;
  const perm = APPROVE_ROLE_PERMISSION[requiredRole];
  if (!perm) {
    throw new ForbiddenException(`Unknown approval role: ${requiredRole}`);
  }
  assertFreightPermission(user, perm);
}
