import { isDecimalString } from "@sdp/solana/amount";
import { TOKEN_TRANSACTION_STATUSES, TOKEN_TRANSACTION_TYPES } from "@sdp/types";
import { z } from "zod";
import {
  assertAssetTypeSupported,
  assetCategorySchema,
  assetTypeSchema,
  issuanceMetadataSchema,
} from "../asset-profiles/schemas";

// ═══════════════════════════════════════════════════════════════════════════
// Extension Schemas
// ═══════════════════════════════════════════════════════════════════════════

const transferFeeConfigSchema = z.object({
  basisPoints: z.number().int().min(0).max(10000),
  maxFee: z.string().refine((value) => isDecimalString(value), {
    message: "Invalid amount format",
  }),
  transferFeeConfigAuthority: z.string().optional(),
  withdrawWithheldAuthority: z.string().optional(),
});

const interestBearingConfigSchema = z.object({
  rate: z.number(),
  rateAuthority: z.string().optional(),
});

const pausableConfigSchema = z.object({
  authority: z.string().min(32).max(44).optional(),
});

const scaledUiAmountConfigSchema = z.object({
  authority: z.string().min(32).max(44).optional(),
  multiplier: z.number().positive().optional(),
  newMultiplier: z.number().positive().optional(),
  newMultiplierEffectiveTimestamp: z.number().int().nonnegative().optional(),
});

const transferHookConfigSchema = z.object({
  programId: z.string().min(32).max(44),
  authority: z.string().min(32).max(44).optional(),
});

// Each extension can be: true/false (enable/disable) or a config object for custom settings
const extensionOverridesSchema = z
  .object({
    transferFee: z.union([z.literal(false), transferFeeConfigSchema]).optional(),
    interestBearing: z.union([z.literal(false), interestBearingConfigSchema]).optional(),
    permanentDelegate: z.union([z.literal(false), z.string()]).optional(),
    pausable: z.union([z.literal(false), pausableConfigSchema]).optional(),
    nonTransferable: z.boolean().optional(),
    defaultAccountState: z.enum(["initialized", "frozen"]).optional(),
    scaledUiAmount: z.union([z.literal(false), scaledUiAmountConfigSchema]).optional(),
    transferHook: z.union([z.literal(false), transferHookConfigSchema]).optional(),
  })
  .strict();

const templateOverridesSchema = z.object({
  extensions: extensionOverridesSchema.optional(),
  requiresAllowlist: z.boolean().optional(),
});

// ═══════════════════════════════════════════════════════════════════════════
// Token Schemas
// ═══════════════════════════════════════════════════════════════════════════

// Normalize legacy template names (tokenized_security, rwa) to canonical form
export const tokenTemplateSchema = z.preprocess(
  (value) => (value === "tokenized_security" || value === "rwa" ? "tokenized-security" : value),
  z.enum(["stablecoin", "arcade", "tokenized-security", "custom"])
);

// Supports template mode with optional overrides for customization

export const createTokenSchema = z.object({
  name: z.string().min(1).max(100),
  symbol: z
    .string()
    .min(1)
    .max(10)
    .regex(/^[A-Za-z0-9.]+$/),
  signingWalletId: z.string().min(1).optional(),
  decimals: z.number().int().min(0).max(18).optional(),
  description: z.string().max(500).optional(),
  uri: z.string().url().optional(),
  imageUrl: z.string().url().optional(),
  maxSupply: z
    .string()
    .refine((value) => isDecimalString(value), { message: "Invalid amount format" })
    .optional(),
  /** Token template - defaults to "custom" if not specified */
  template: tokenTemplateSchema.optional(),
  /** Template overrides for customization */
  overrides: templateOverridesSchema.optional(),
  requiresAllowlist: z.boolean().optional(),
  isMintable: z.boolean().optional(),
  isFreezable: z.boolean().optional(),
});

export type CreateTokenInput = z.infer<typeof createTokenSchema>;

// Body for POST /v1/issuance/tokens/asset-profile: the full token-create payload
// plus the asset-profile fields, so a token and its profile are created together.
// `superRefine` enforces the same category<->type consistency as the standalone
// asset-profile create (public metadata is server-computed, never accepted here).
export const createTokenWithAssetProfileSchema = createTokenSchema
  .extend({
    assetCategory: assetCategorySchema.default("generic"),
    assetType: assetTypeSchema.default("generic"),
    issuanceMetadata: issuanceMetadataSchema.optional(),
  })
  .superRefine(assertAssetTypeSupported);

export type CreateTokenWithAssetProfileInput = z.infer<typeof createTokenWithAssetProfileSchema>;

export const updateTokenSchema = z.object({
  name: z.string().min(1).max(100).optional(),
  // Symbol and decimals define the mint; the handler rejects them after deploy.
  // Same constraints as createTokenSchema.
  symbol: z
    .string()
    .min(1)
    .max(10)
    .regex(/^[A-Za-z0-9.]+$/)
    .optional(),
  decimals: z.number().int().min(0).max(18).optional(),
  description: z.string().max(500).nullable().optional(),
  uri: z.string().url().nullable().optional(),
  imageUrl: z.string().url().nullable().optional(),
  status: z.enum(["active", "paused"]).optional(),
  // Access-control enforcement input for deploy; only accepted while the token
  // is still undeployed (the handler rejects it after deployment).
  requiresAllowlist: z.boolean().optional(),
  // SPL has no supply-cap field: the cap is enforced by SDP while it holds the
  // mint authority, so it stays editable until the supply is locked on-chain
  // (the handler rejects it once the authority is gone). null = uncapped.
  maxSupply: z
    .string()
    .refine((value) => isDecimalString(value), { message: "Invalid amount format" })
    .nullable()
    .optional(),
});

export const mintSchema = z.object({
  signingWalletId: z.string().min(1).optional(),
  mint: z.object({
    destination: z.string().min(32).max(44),
    amount: z
      .string()
      .refine((value) => isDecimalString(value), { message: "Invalid amount format" }),
    memo: z.string().max(100).optional(),
  }),
  options: z
    .object({
      priorityFee: z
        .union([z.enum(["none", "low", "medium", "high"]), z.number().int().min(0)])
        .optional(),
      simulate: z.boolean().optional(),
    })
    .optional(),
});

export const burnSchema = z.object({
  signingWalletId: z.string().min(1).optional(),
  burn: z.object({
    source: z.string().min(32).max(44),
    amount: z
      .string()
      .refine((value) => isDecimalString(value), { message: "Invalid amount format" }),
    memo: z.string().max(100).optional(),
  }),
  options: z
    .object({
      priorityFee: z
        .union([z.enum(["none", "low", "medium", "high"]), z.number().int().min(0)])
        .optional(),
      simulate: z.boolean().optional(),
    })
    .optional(),
});

export const seizeSchema = z.object({
  signingWalletId: z.string().min(1).optional(),
  seize: z.object({
    source: z.string().min(32).max(44),
    destination: z.string().min(32).max(44),
    amount: z
      .string()
      .refine((value) => isDecimalString(value), { message: "Invalid amount format" }),
    delegateAuthority: z.string().min(32).max(44).optional(),
    memo: z.string().max(100).optional(),
  }),
  options: z
    .object({
      priorityFee: z
        .union([z.enum(["none", "low", "medium", "high"]), z.number().int().min(0)])
        .optional(),
      simulate: z.boolean().optional(),
    })
    .optional(),
});

export const forceBurnSchema = z.object({
  signingWalletId: z.string().min(1).optional(),
  forceBurn: z.object({
    source: z.string().min(32).max(44),
    amount: z
      .string()
      .refine((value) => isDecimalString(value), { message: "Invalid amount format" }),
    delegateAuthority: z.string().min(32).max(44).optional(),
    memo: z.string().max(100).optional(),
  }),
  options: z
    .object({
      priorityFee: z
        .union([z.enum(["none", "low", "medium", "high"]), z.number().int().min(0)])
        .optional(),
      simulate: z.boolean().optional(),
    })
    .optional(),
});

export const updateAuthoritySchema = z.object({
  signingWalletId: z.string().min(1).optional(),
  authority: z.object({
    role: z.enum(["mint", "freeze", "permanentDelegate", "metadata"]),
    currentAuthority: z.string().min(32).max(44).optional(),
    newAuthority: z.string().min(32).max(44).nullable(),
  }),
  options: z
    .object({
      priorityFee: z
        .union([z.enum(["none", "low", "medium", "high"]), z.number().int().min(0)])
        .optional(),
      simulate: z.boolean().optional(),
    })
    .optional(),
});

export const deployTokenSchema = z.object({
  signingWalletId: z.string().min(1).optional(),
  feePayment: z.enum(["sponsored", "wallet"]).default("sponsored"),
});

// Records a confirmed non-custodial deploy: the client sends the `mint` it
// received from `deploy/prepare` plus the signature of the create tx it
// submitted, so the server can verify the tx landed and persist the mint.
// `listAddress` and `signingWalletId` are accepted for backward compatibility
// but ignored — the server re-derives the ABL list PDA from the mint authority
// and uses the signing wallet pinned at deploy/prepare, so neither can be
// changed at confirm time.
export const confirmDeploySchema = z.object({
  signature: z.string().min(1),
  mint: z.string().min(32).max(44),
  listAddress: z.string().min(32).max(44).optional(),
  signingWalletId: z.string().min(1).optional(),
});

export const pauseTokenSchema = z.object({
  options: z
    .object({
      priorityFee: z
        .union([z.enum(["none", "low", "medium", "high"]), z.number().int().min(0)])
        .optional(),
      simulate: z.boolean().optional(),
    })
    .optional(),
});

export const freezeSchema = z.object({
  accountAddress: z.string().min(32).max(44),
  reason: z.string().max(500).optional(),
  signingWalletId: z.string().min(1).optional(),
});

export const unfreezeSchema = z.object({
  accountAddress: z.string().min(32).max(44),
  signingWalletId: z.string().min(1).optional(),
});

export const addAllowlistSchema = z.object({
  address: z.string().min(32).max(44),
  label: z.string().max(100).optional(),
});

export const listAllowlistQuerySchema = z.object({
  page: z.coerce.number().int().positive().default(1),
  pageSize: z.coerce.number().int().min(1).max(500).default(50),
  // Contains-style search over address + label (see listAllowlistEntries).
  search: z.string().trim().min(1).max(100).optional(),
  // Exact-match label filter (values come from the labels facet endpoint).
  label: z.string().trim().min(1).max(100).optional(),
});

export const listTokenTransactionsQuerySchema = z.object({
  page: z.coerce.number().int().positive().default(1),
  // Capped at 500 so the dashboard's grow-window list can't blow up the client.
  pageSize: z.coerce.number().int().min(1).max(500).default(50),
  // Optional exact-match filters; ordering stays newest-first server-side.
  status: z.enum(TOKEN_TRANSACTION_STATUSES).optional(),
  type: z.enum(TOKEN_TRANSACTION_TYPES).optional(),
});

// ═══════════════════════════════════════════════════════════════════════════
// Token List Query
// ═══════════════════════════════════════════════════════════════════════════

const tokenListTimestampSchema = z
  .string()
  .datetime({ offset: true })
  .transform((value) => new Date(value).toISOString());

/**
 * Lifecycle state as the dashboard presents it, which is *not* the raw `status`
 * column: a token is a draft until it has a mint (or a deploy timestamp),
 * paused once its on-chain pausable extension is engaged, and active otherwise.
 * Kept as its own filter so `status` stays a faithful column filter for API
 * consumers.
 */
export const tokenDeploymentStatusSchema = z.enum(["draft", "active", "paused"]);

/**
 * Whitelisted sort keys. The service maps these to physical columns — the raw
 * value never reaches SQL — and every ordering carries an `id` tiebreaker so
 * paging stays stable across equal keys.
 */
export const tokenListSortBySchema = z.enum(["createdAt", "name"]);

export const listTokensQuerySchema = z.object({
  // Bounded, not just positive: offset paging turns the page number into work the
  // database has to walk past, and no project is a million pages deep. Keeps a
  // crafted `?page=9007199254740991` from becoming a deep-offset scan (and its
  // offset arithmetic from leaving safe-integer range).
  page: z.coerce.number().int().positive().max(1_000_000).default(1),
  // 100 keeps the extension-hydration fan-out and the response body bounded;
  // the dashboard pages well under it.
  pageSize: z.coerce.number().int().min(1).max(100).default(50),
  // Contains-style search over name + symbol + mint address + id. Blank is
  // accepted (an empty input is not an error) and treated as "no filter", so a
  // client can bind this straight to a text input. Needles under 3 characters
  // can't use the trigram index and fall back to a filtered scan, which is fine
  // at this table's per-project size.
  search: z.string().trim().max(100).optional(),
  status: z.enum(["pending", "active", "paused", "revoked"]).optional(),
  deploymentStatus: tokenDeploymentStatusSchema.optional(),
  // Exact match against the stored template id. Not an enum: rows predating the
  // current catalog hold legacy ids (`rwa`, `tokenized_security`), and the
  // facets endpoint hands the client the values actually present.
  template: z.string().trim().min(1).max(64).optional(),
  createdAfter: tokenListTimestampSchema.optional(),
  createdBefore: tokenListTimestampSchema.optional(),
  sortBy: tokenListSortBySchema.default("createdAt"),
  sortDirection: z.enum(["asc", "desc"]).default("desc"),
});
