import {
  getSelectableCandidateIds,
  getSelectableCandidateIdsForTarget,
  mapAwsDiscovery,
  mapGcpDiscovery,
} from "@/actions/organizations/organizations.adapter";
import {
  AwsDiscoveryResult,
  GcpDiscoveryResult,
  OrgFlowType,
  OrgHierarchy,
  ORG_SECRET_TYPE,
  ORGANIZATION_TYPE,
  OrgSecretPayload,
} from "@/types/organizations";

interface BaseOrgSetupData {
  /** Optional display name; falls back to the external id. */
  organizationName?: string;
}

export interface AwsOrgSetupData extends BaseOrgSetupData {
  orgType: typeof ORGANIZATION_TYPE.AWS;
  /** AWS organization id (`o-…`) — the external id the organization matches on. */
  awsOrgId: string;
  roleArn: string;
  // OU or root ID the StackSet was deployed to — scopes the default selection.
  organizationalUnitId?: string;
}

export interface GcpOrgSetupData extends BaseOrgSetupData {
  orgType: typeof ORGANIZATION_TYPE.GCP;
  /** Numeric Google Cloud organization id — the external id matched on. */
  gcpOrgId: string;
  /** API secret vocabulary — `service_account` (underscore) or `static`. */
  credentialMethod:
    | typeof ORG_SECRET_TYPE.SERVICE_ACCOUNT
    | typeof ORG_SECRET_TYPE.STATIC;
  /** Service-account key pasted as JSON (validated by the form before submit). */
  serviceAccountKey?: string;
  clientId?: string;
  clientSecret?: string;
  refreshToken?: string;
}

/**
 * Values collected by an organization setup form, tagged with the organization
 * type that produced them: each form fills its own arm, and the strategy is
 * picked from the tag, so the fields and the credentials built from them cannot
 * belong to different types.
 */
export type OrgSetupSubmissionData = AwsOrgSetupData | GcpOrgSetupData;

export type OrgSetupErrorField =
  | "organizationName"
  | "awsOrgId"
  | "gcpOrgId"
  | "serviceAccountKey"
  | "clientId"
  | "clientSecret"
  | "refreshToken";

/**
 * Per-organization-type pieces of the shared setup submission chain
 * (find-or-create org → create/replace secret → discover → poll → select).
 * Shared machinery (ordering, polling, cancellation) stays in the hook; only
 * these type-specific bits are dispatched on the discriminant.
 */
interface OrgSetupStrategy<D extends OrgSetupSubmissionData> {
  orgType: D["orgType"];
  /** Form field the organization external-id error attaches to. */
  externalIdField: OrgSetupErrorField;
  /** External id used to match/create the organization. */
  getExternalId: (data: D) => string;
  /** Display name to store (falls back to the external id). */
  getResolvedName: (data: D) => string;
  /**
   * Credential payload for the organization secret. `stackSetExternalId` is the
   * tenant id AWS trusts as `sts:ExternalId` — not `getExternalId`'s
   * organization external id.
   */
  buildSecretPayload: (data: D, stackSetExternalId: string) => OrgSecretPayload;
  /**
   * Maps a secret-scoped server error to the form field it belongs to, or null to
   * surface it in the banner. Matched on names rather than the pointer, which may
   * stop at `/data/attributes/secret` and leave the field as the error's own key.
   */
  mapSecretErrorField: (fieldNames: string) => OrgSetupErrorField | null;
  /**
   * Normalize the raw discovery result into the common hierarchy model and pick
   * the candidates to pre-select.
   */
  ingestDiscovery: (
    rawResult: unknown,
    data: D,
  ) => { hierarchy: OrgHierarchy; defaultSelection: string[] };
  /** Copy shown when discovery reports/looks like an auth failure. */
  authFailureMessage: (detail?: string) => string;
}

/**
 * Human copy for the machine codes a failed discovery reports in
 * `attributes.error`. The code decides the framing too: only some of them are
 * credential problems, so "Authentication failed…" is wrong for the rest.
 */
const DISCOVERY_ERROR_COPY: Record<string, string> = {
  gcp_invalid_organization_id:
    "That organization ID is not valid. Copy the numeric ID from the Google Cloud console and try again.",
  gcp_organization_not_found:
    "No organization with that ID was found. Check the ID, and that the service account has been granted access to the organization.",
  gcp_insufficient_permissions:
    "The service account cannot list this organization's folders and projects. Grant it the Folder Viewer and Project Viewer roles at the organization level, then try again.",
  gcp_service_unavailable:
    "Google Cloud did not respond while reading the organization. Nothing is wrong with your credentials — try again in a few minutes.",
  hierarchy_depth_exceeded:
    "This organization's folder hierarchy is deeper than Prowler can read. Contact support so we can help you onboard it.",
};

/**
 * Copy for a failed discovery. An unknown code falls back to the type's
 * auth-failure copy without the raw token, which is a support detail.
 */
function describeDiscoveryFailure(
  code: string | undefined,
  authFailure: string,
): string {
  const trimmedCode = code?.trim();
  if (!trimmedCode) {
    return authFailure;
  }

  return DISCOVERY_ERROR_COPY[trimmedCode] ?? authFailure;
}

/**
 * A strategy with its submission data already applied, so the hook never holds a
 * strategy and a data object it could pair with the wrong type.
 */
export interface BoundOrgSetupStrategy {
  orgType: OrgFlowType;
  externalIdField: OrgSetupErrorField;
  /** External id used to match/create the organization. */
  externalId: string;
  resolvedName: string;
  buildSecretPayload: (stackSetExternalId: string) => OrgSecretPayload;
  mapSecretErrorField: (fieldNames: string) => OrgSetupErrorField | null;
  ingestDiscovery: (rawResult: unknown) => {
    hierarchy: OrgHierarchy;
    defaultSelection: string[];
  };
  authFailureMessage: (detail?: string) => string;
  /** Copy for a discovery that failed with a machine error code. */
  discoveryFailureMessage: (code?: string) => string;
}

const AWS_AUTH_FAILURE =
  "Authentication failed. Please verify the StackSet deployment and Role ARN, then try again.";

const awsOrgSetupStrategy: OrgSetupStrategy<AwsOrgSetupData> = {
  orgType: ORGANIZATION_TYPE.AWS,
  externalIdField: "awsOrgId",
  getExternalId: (data) => data.awsOrgId,
  getResolvedName: (data) => data.organizationName?.trim() || data.awsOrgId,
  buildSecretPayload: (data, stackSetExternalId) => ({
    secretType: ORG_SECRET_TYPE.ROLE,
    secret: {
      role_arn: data.roleArn,
      external_id: stackSetExternalId,
    },
  }),
  // AWS role-secret field errors surface in the banner (no dedicated fields).
  mapSecretErrorField: () => null,
  ingestDiscovery: (rawResult, data) => {
    const hierarchy = mapAwsDiscovery(rawResult as AwsDiscoveryResult);
    // The deployment (management/delegated admin) account is where the local
    // role is created; its ID is the one embedded in the Role ARN.
    const deploymentCandidateId = data.roleArn.match(
      /^arn:aws:iam::(\d{12}):role\//,
    )?.[1];

    return {
      hierarchy,
      defaultSelection: getSelectableCandidateIdsForTarget(
        hierarchy,
        data.organizationalUnitId ?? "",
        deploymentCandidateId,
      ),
    };
  },
  authFailureMessage: (detail) =>
    detail ? `${AWS_AUTH_FAILURE} ${detail}` : AWS_AUTH_FAILURE,
};

const GCP_AUTH_FAILURE =
  "Authentication failed. Please verify the service account permissions or credentials, then try again.";

const gcpOrgSetupStrategy: OrgSetupStrategy<GcpOrgSetupData> = {
  orgType: ORGANIZATION_TYPE.GCP,
  externalIdField: "gcpOrgId",
  getExternalId: (data) => data.gcpOrgId.trim(),
  getResolvedName: (data) =>
    data.organizationName?.trim() || data.gcpOrgId.trim(),
  buildSecretPayload: (data) => {
    if (data.credentialMethod === ORG_SECRET_TYPE.STATIC) {
      return {
        secretType: ORG_SECRET_TYPE.STATIC,
        secret: {
          client_id: data.clientId?.trim() ?? "",
          client_secret: data.clientSecret?.trim() ?? "",
          refresh_token: data.refreshToken?.trim() ?? "",
        },
      };
    }
    // The form validates this JSON before submit, so the parse cannot throw here.
    return {
      secretType: ORG_SECRET_TYPE.SERVICE_ACCOUNT,
      secret: {
        service_account_key: JSON.parse(data.serviceAccountKey ?? "{}"),
      },
    };
  },
  mapSecretErrorField: (fieldNames) => {
    if (fieldNames.includes("service_account_key")) return "serviceAccountKey";
    if (fieldNames.includes("client_id")) return "clientId";
    if (fieldNames.includes("client_secret")) return "clientSecret";
    if (fieldNames.includes("refresh_token")) return "refreshToken";
    return null;
  },
  ingestDiscovery: (rawResult) => {
    const hierarchy = mapGcpDiscovery(rawResult as GcpDiscoveryResult);

    // GCP has no StackSet-style target scoping, so the default is every ready
    // project; folder ancestors are derived server-side.
    return {
      hierarchy,
      defaultSelection: getSelectableCandidateIds(hierarchy),
    };
  },
  authFailureMessage: (detail) =>
    detail ? `${GCP_AUTH_FAILURE} ${detail}` : GCP_AUTH_FAILURE,
};

function bind<D extends OrgSetupSubmissionData>(
  strategy: OrgSetupStrategy<D>,
  data: D,
): BoundOrgSetupStrategy {
  return {
    orgType: strategy.orgType,
    externalIdField: strategy.externalIdField,
    externalId: strategy.getExternalId(data),
    resolvedName: strategy.getResolvedName(data),
    buildSecretPayload: (stackSetExternalId) =>
      strategy.buildSecretPayload(data, stackSetExternalId),
    mapSecretErrorField: strategy.mapSecretErrorField,
    ingestDiscovery: (rawResult) => strategy.ingestDiscovery(rawResult, data),
    authFailureMessage: strategy.authFailureMessage,
    discoveryFailureMessage: (code) =>
      describeDiscoveryFailure(code, strategy.authFailureMessage()),
  };
}

/**
 * Binds the submission data to the strategy its own tag names. The switch has no
 * default, so a new organization type is a compile error until it brings one.
 */
export function bindOrgSetupStrategy(
  data: OrgSetupSubmissionData,
): BoundOrgSetupStrategy {
  switch (data.orgType) {
    case ORGANIZATION_TYPE.AWS:
      return bind(awsOrgSetupStrategy, data);
    case ORGANIZATION_TYPE.GCP:
      return bind(gcpOrgSetupStrategy, data);
  }
}
