import { randomBytes } from "node:crypto";

/**
 * Generate the external ID used in the cross-account role's trust policy. This is
 * the confused-deputy guard: the customer's role only trusts our account when the
 * caller presents this exact value, so it must be unguessable and unique per
 * connection. base64url, no padding.
 */
export function generateExternalId(): string {
  return randomBytes(32).toString("base64url");
}

export type QuickCreateUrlInput = {
  /** Region the stack is created in. Metric streams / Firehose are regional. */
  region: string;
  /** Public HTTPS URL of the CloudFormation template (e.g. served from S3). */
  templateUrl: string;
  /** Default stack name shown to the user (they can change it). */
  stackName: string;
  /** CloudFormation parameter values; each is sent as `param_<Key>`. */
  params: Record<string, string>;
};

/**
 * Build a CloudFormation "Launch Stack" quick-create console link. The console
 * keeps its SPA routing + stack parameters in the URL fragment, so everything
 * after the host lives in the hash. Values are URL-encoded.
 *
 * The prod account ID is passed as a *parameter value* (not baked into the
 * template), so the committed template stays free of prod specifics.
 */
/** AWS region codes are lowercase letters, digits, and hyphens — nothing else. */
export const AWS_REGION_RE = /^[a-z0-9-]{1,32}$/;

export function buildConnectQuickCreateUrl(input: QuickCreateUrlInput): string {
  // `region` is interpolated into the hostname; reject anything that could break
  // out of `<region>.console.aws.amazon.com` and point the link at another host.
  if (!AWS_REGION_RE.test(input.region)) {
    throw new Error(`invalid region: ${input.region}`);
  }
  const base = `https://${input.region}.console.aws.amazon.com/cloudformation/home?region=${encodeURIComponent(
    input.region,
  )}`;

  const frag = new URLSearchParams();
  frag.set("templateURL", input.templateUrl);
  frag.set("stackName", input.stackName);
  for (const [key, value] of Object.entries(input.params)) {
    frag.set(`param_${key}`, value);
  }

  return `${base}#/stacks/quickcreate?${frag.toString()}`;
}

/** Pull the 12-digit account id out of a role ARN, or null if it isn't one. */
export function parseAccountIdFromRoleArn(arn: string): string | null {
  const m = /^arn:aws[a-z-]*:iam::(\d{12}):role\/.+$/.exec(arn);
  return m?.[1] ?? null;
}

/**
 * Port: assume the cross-account role with the external ID and resolve the caller
 * identity. The real adapter wraps @aws-sdk/client-sts (AssumeRole +
 * GetCallerIdentity); tests pass a fake. Throws if the role can't be assumed
 * (stack not deployed yet, trust/externalId wrong, etc.).
 */
export type StsVerifier = {
  verifyAssumeRole(input: {
    roleArn: string;
    externalId: string;
  }): Promise<{ accountId: string }>;
};

export type VerifyResult =
  | { status: "connected"; accountId: string }
  | { status: "account_mismatch"; accountId: string; expectedAccountId: string }
  | { status: "failed"; reason: string };

/**
 * Health-check a connection: assume the role and confirm the resolved account
 * matches the one named in the role ARN. Never throws — a failure to assume is a
 * normal "not connected yet" state the UI renders, not an error.
 */
export async function verifyConnection(
  input: { roleArn: string; externalId: string },
  sts: StsVerifier,
): Promise<VerifyResult> {
  const expectedAccountId = parseAccountIdFromRoleArn(input.roleArn);
  try {
    const { accountId } = await sts.verifyAssumeRole(input);
    if (expectedAccountId && accountId !== expectedAccountId) {
      return { status: "account_mismatch", accountId, expectedAccountId };
    }
    return { status: "connected", accountId };
  } catch (err) {
    return {
      status: "failed",
      reason: err instanceof Error ? err.message : String(err),
    };
  }
}
