import { z } from 'zod';

/** Discord MessageFlags.IsComponentsV2 */
export const COMPONENTS_V2_FLAG = 32768 as const;

export const ComponentV2SourceSchema = z.enum(['w', 'l', 't', 's', 'm', 'a']);
export type ComponentV2Source = z.infer<typeof ComponentV2SourceSchema>;

export const ComponentActionTypeSchema = z.enum([
  'EPHEMERAL_REPLY',
  'PUBLIC_REPLY',
  'ASSIGN_ROLE',
  'REMOVE_ROLE',
  'TOGGLE_ROLE',
  'ASSIGN_SELECTED_ROLES',
  'NONE'
]);
export type ComponentActionType = z.infer<typeof ComponentActionTypeSchema>;

export const ComponentActionSchema = z
  .object({
    type: ComponentActionTypeSchema,
    content: z.string().max(2000).optional(),
    roleId: z.string().max(32).optional()
  })
  .superRefine((value, ctx) => {
    if (
      (value.type === 'EPHEMERAL_REPLY' || value.type === 'PUBLIC_REPLY') &&
      !value.content?.trim()
    ) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: 'Reply actions require content',
        path: ['content']
      });
    }
    if (
      (value.type === 'ASSIGN_ROLE' ||
        value.type === 'REMOVE_ROLE' ||
        value.type === 'TOGGLE_ROLE') &&
      !value.roleId?.trim()
    ) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: 'Role actions require roleId',
        path: ['roleId']
      });
    }
  });
export type ComponentAction = z.infer<typeof ComponentActionSchema>;

export const ComponentButtonStyleSchema = z.enum([
  'Primary',
  'Secondary',
  'Success',
  'Danger',
  'Link'
]);
export type ComponentButtonStyle = z.infer<typeof ComponentButtonStyleSchema>;

export const SeparatorSpacingSchema = z.enum(['Small', 'Large']);
export type SeparatorSpacing = z.infer<typeof SeparatorSpacingSchema>;

export const MediaGalleryItemSchema = z.object({
  url: z.string().max(2048),
  description: z.string().max(1024).optional(),
  spoiler: z.boolean().optional()
});
export type MediaGalleryItem = z.infer<typeof MediaGalleryItemSchema>;

/** Plain object (no effects) so it can sit in Zod discriminated unions. */
export const ComponentButtonSchema = z.object({
  type: z.literal('button'),
  id: z.string().min(1).max(40).optional(),
  style: ComponentButtonStyleSchema.default('Secondary'),
  label: z.string().min(1).max(80),
  emoji: z.string().max(64).optional(),
  url: z.string().max(2048).optional(),
  actionId: z.string().min(1).max(40).optional(),
  disabled: z.boolean().optional()
});
export type ComponentButton = z.infer<typeof ComponentButtonSchema>;

export const ComponentThumbnailSchema = z.object({
  type: z.literal('thumbnail'),
  id: z.string().min(1).max(40).optional(),
  url: z.string().max(2048),
  description: z.string().max(1024).optional(),
  spoiler: z.boolean().optional()
});
export type ComponentThumbnail = z.infer<typeof ComponentThumbnailSchema>;

export const StringSelectOptionSchema = z.object({
  label: z.string().min(1).max(100),
  value: z.string().min(1).max(100),
  description: z.string().max(100).optional(),
  emoji: z.string().max(64).optional(),
  actionId: z.string().min(1).max(40).optional()
});
export type StringSelectOption = z.infer<typeof StringSelectOptionSchema>;

export const ComponentStringSelectSchema = z.object({
  type: z.literal('string_select'),
  id: z.string().min(1).max(40).optional(),
  placeholder: z.string().max(150).optional(),
  minValues: z.number().int().min(0).max(25).optional(),
  maxValues: z.number().int().min(1).max(25).optional(),
  options: z.array(StringSelectOptionSchema).min(1).max(25),
  actionId: z.string().min(1).max(40).optional(),
  disabled: z.boolean().optional()
});
export type ComponentStringSelect = z.infer<typeof ComponentStringSelectSchema>;

const EntitySelectBase = {
  id: z.string().min(1).max(40).optional(),
  placeholder: z.string().max(150).optional(),
  minValues: z.number().int().min(0).max(25).optional(),
  maxValues: z.number().int().min(1).max(25).optional(),
  actionId: z.string().min(1).max(40),
  disabled: z.boolean().optional()
};

export const ComponentUserSelectSchema = z.object({
  type: z.literal('user_select'),
  ...EntitySelectBase
});
export type ComponentUserSelect = z.infer<typeof ComponentUserSelectSchema>;

export const ComponentRoleSelectSchema = z.object({
  type: z.literal('role_select'),
  ...EntitySelectBase
});
export type ComponentRoleSelect = z.infer<typeof ComponentRoleSelectSchema>;

export const ComponentChannelSelectSchema = z.object({
  type: z.literal('channel_select'),
  ...EntitySelectBase
});
export type ComponentChannelSelect = z.infer<typeof ComponentChannelSelectSchema>;

export const ComponentMentionableSelectSchema = z.object({
  type: z.literal('mentionable_select'),
  ...EntitySelectBase
});
export type ComponentMentionableSelect = z.infer<typeof ComponentMentionableSelectSchema>;

export const ComponentSelectSchema = z.discriminatedUnion('type', [
  ComponentStringSelectSchema,
  ComponentUserSelectSchema,
  ComponentRoleSelectSchema,
  ComponentChannelSelectSchema,
  ComponentMentionableSelectSchema
]);
export type ComponentSelect = z.infer<typeof ComponentSelectSchema>;

export const ComponentTextDisplaySchema = z.object({
  type: z.literal('text_display'),
  id: z.string().min(1).max(40).optional(),
  content: z.string().min(1).max(4000)
});
export type ComponentTextDisplay = z.infer<typeof ComponentTextDisplaySchema>;

export const ComponentSeparatorSchema = z.object({
  type: z.literal('separator'),
  id: z.string().min(1).max(40).optional(),
  divider: z.boolean().optional(),
  spacing: SeparatorSpacingSchema.optional()
});
export type ComponentSeparator = z.infer<typeof ComponentSeparatorSchema>;

export const ComponentMediaGallerySchema = z.object({
  type: z.literal('media_gallery'),
  id: z.string().min(1).max(40).optional(),
  items: z.array(MediaGalleryItemSchema).min(1).max(10)
});
export type ComponentMediaGallery = z.infer<typeof ComponentMediaGallerySchema>;

export const ComponentSectionAccessorySchema = z.discriminatedUnion('type', [
  ComponentThumbnailSchema,
  ComponentButtonSchema
]);

export const ComponentSectionSchema = z.object({
  type: z.literal('section'),
  id: z.string().min(1).max(40).optional(),
  text: z.union([z.string().min(1).max(4000), z.array(z.string().min(1).max(4000)).min(1).max(3)]),
  accessory: ComponentSectionAccessorySchema
});
export type ComponentSection = z.infer<typeof ComponentSectionSchema>;

export const ComponentActionRowChildSchema = z.union([
  ComponentButtonSchema,
  ComponentStringSelectSchema,
  ComponentUserSelectSchema,
  ComponentRoleSelectSchema,
  ComponentChannelSelectSchema,
  ComponentMentionableSelectSchema
]);

export const ComponentActionRowSchema = z.object({
  type: z.literal('action_row'),
  id: z.string().min(1).max(40).optional(),
  components: z.array(ComponentActionRowChildSchema).min(1).max(5)
});
export type ComponentActionRow = z.infer<typeof ComponentActionRowSchema>;

export type ComponentV2Node =
  | ComponentTextDisplay
  | ComponentSeparator
  | ComponentThumbnail
  | ComponentMediaGallery
  | ComponentSection
  | ComponentActionRow
  | ComponentButton
  | ComponentSelect
  | {
      type: 'container';
      id?: string;
      accentColor?: number;
      spoiler?: boolean;
      components: ComponentV2Node[];
    };

type ComponentContainer = Extract<ComponentV2Node, { type: 'container' }>;

const leafComponentSchemas = [
  ComponentTextDisplaySchema,
  ComponentSeparatorSchema,
  ComponentThumbnailSchema,
  ComponentMediaGallerySchema,
  ComponentSectionSchema,
  ComponentActionRowSchema,
  ComponentButtonSchema,
  ComponentStringSelectSchema,
  ComponentUserSelectSchema,
  ComponentRoleSelectSchema,
  ComponentChannelSelectSchema,
  ComponentMentionableSelectSchema
] as const;

export const ComponentContainerSchema: z.ZodType<ComponentContainer> = z.lazy(() =>
  z.object({
    type: z.literal('container'),
    id: z.string().min(1).max(40).optional(),
    accentColor: z.number().int().min(0).max(0xffffff).optional(),
    spoiler: z.boolean().optional(),
    components: z.array(ComponentV2NodeSchema).min(1).max(40)
  })
) as z.ZodType<ComponentContainer>;

export const ComponentV2NodeSchema: z.ZodType<ComponentV2Node> = z.lazy(() =>
  z.union([...leafComponentSchemas, ComponentContainerSchema])
) as z.ZodType<ComponentV2Node>;

const MAX_COMPONENTS = 40;

/** Counts every node in the tree (including nested container children). */
export function countComponentsV2(nodes: ComponentV2Node[]): number {
  let count = 0;
  for (const node of nodes) {
    count += 1;
    if (node.type === 'container') {
      count += countComponentsV2(node.components);
    } else if (node.type === 'action_row') {
      count += node.components.length;
    } else if (node.type === 'section') {
      count += 1;
    } else if (node.type === 'media_gallery') {
      count += node.items.length;
    }
  }
  return count;
}

function collectActionIds(nodes: ComponentV2Node[], ids: Set<string>): void {
  for (const node of nodes) {
    switch (node.type) {
      case 'button':
        if (node.actionId) {
          ids.add(node.actionId);
        }
        break;
      case 'string_select':
        if (node.actionId) {
          ids.add(node.actionId);
        }
        for (const option of node.options) {
          if (option.actionId) {
            ids.add(option.actionId);
          }
        }
        break;
      case 'user_select':
      case 'role_select':
      case 'channel_select':
      case 'mentionable_select':
        ids.add(node.actionId);
        break;
      case 'action_row':
        collectActionIds(node.components as ComponentV2Node[], ids);
        break;
      case 'section':
        if (node.accessory.type === 'button' && node.accessory.actionId) {
          ids.add(node.accessory.actionId);
        }
        break;
      case 'container':
        collectActionIds(node.components, ids);
        break;
      default:
        break;
    }
  }
}

function validateButtonRules(
  button: ComponentButton,
  ctx: z.RefinementCtx,
  path: (string | number)[]
): void {
  if (button.style === 'Link') {
    if (!button.url?.trim()) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: 'Link buttons require url',
        path: [...path, 'url']
      });
    }
  } else if (!button.actionId?.trim()) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: 'Interactive buttons require actionId',
      path: [...path, 'actionId']
    });
  }
}

function validateNodeRules(
  nodes: ComponentV2Node[],
  ctx: z.RefinementCtx,
  path: (string | number)[]
): void {
  nodes.forEach((node, index) => {
    const nodePath = [...path, index];
    if (node.type === 'button') {
      validateButtonRules(node, ctx, nodePath);
    } else if (node.type === 'action_row') {
      node.components.forEach((child, childIndex) => {
        if (child.type === 'button') {
          validateButtonRules(child, ctx, [...nodePath, 'components', childIndex]);
        }
      });
    } else if (node.type === 'section' && node.accessory.type === 'button') {
      validateButtonRules(node.accessory, ctx, [...nodePath, 'accessory']);
    } else if (node.type === 'container') {
      validateNodeRules(node.components, ctx, [...nodePath, 'components']);
    }
  });
}

export const MessageComponentsV2Schema = z
  .object({
    components: z.array(ComponentV2NodeSchema).min(1).max(MAX_COMPONENTS),
    actions: z.record(z.string().min(1).max(40), ComponentActionSchema).default({})
  })
  .superRefine((value, ctx) => {
    const total = countComponentsV2(value.components);
    if (total > MAX_COMPONENTS) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: `At most ${MAX_COMPONENTS} components allowed (got ${total})`,
        path: ['components']
      });
    }

    validateNodeRules(value.components, ctx, ['components']);

    const referenced = new Set<string>();
    collectActionIds(value.components, referenced);
    for (const actionId of referenced) {
      if (!value.actions[actionId]) {
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          message: `Missing action definition for "${actionId}"`,
          path: ['actions', actionId]
        });
      }
    }
  });

export type MessageComponentsV2 = z.infer<typeof MessageComponentsV2Schema>;

export function componentsV2HasContent(
  payload: MessageComponentsV2 | null | undefined
): boolean {
  return Boolean(payload && payload.components.length > 0);
}

/**
 * Applies a string renderer to every text/URL field in a Components V2 tree.
 * Used by Welcome/Tags/Scheduler before handing data to discord.js.
 */
export function mapComponentsV2TextFields(
  payload: MessageComponentsV2,
  render: (value: string) => string
): MessageComponentsV2 {
  const mapOptional = (value: string | undefined): string | undefined => {
    if (!value?.trim()) {
      return undefined;
    }
    return render(value);
  };

  const mapNode = (node: ComponentV2Node): ComponentV2Node => {
    switch (node.type) {
      case 'text_display':
        return { ...node, content: render(node.content) };
      case 'thumbnail':
        return {
          ...node,
          url: render(node.url),
          description: mapOptional(node.description)
        };
      case 'media_gallery':
        return {
          ...node,
          items: node.items.map((item) => ({
            ...item,
            url: render(item.url),
            description: mapOptional(item.description)
          }))
        };
      case 'section': {
        const text = Array.isArray(node.text)
          ? node.text.map((part) => render(part))
          : render(node.text);
        const accessory =
          node.accessory.type === 'thumbnail'
            ? {
                ...node.accessory,
                url: render(node.accessory.url),
                description: mapOptional(node.accessory.description)
              }
            : {
                ...node.accessory,
                label: render(node.accessory.label),
                url: mapOptional(node.accessory.url)
              };
        return { ...node, text, accessory };
      }
      case 'button':
        return {
          ...node,
          label: render(node.label),
          url: mapOptional(node.url)
        };
      case 'string_select':
        return {
          ...node,
          placeholder: mapOptional(node.placeholder),
          options: node.options.map((option) => ({
            ...option,
            label: render(option.label),
            description: mapOptional(option.description)
          }))
        };
      case 'user_select':
      case 'role_select':
      case 'channel_select':
      case 'mentionable_select':
        return { ...node, placeholder: mapOptional(node.placeholder) };
      case 'action_row':
        return {
          ...node,
          components: node.components.map((child) =>
            mapNode(child as ComponentV2Node)
          ) as typeof node.components
        };
      case 'container':
        return {
          ...node,
          components: node.components.map(mapNode)
        };
      case 'separator':
      default:
        return node;
    }
  };

  const actions: MessageComponentsV2['actions'] = {};
  for (const [id, action] of Object.entries(payload.actions)) {
    actions[id] = {
      ...action,
      content: mapOptional(action.content)
    };
  }

  return {
    components: payload.components.map(mapNode),
    actions
  };
}

const CUSTOM_ID_PREFIX = 'cv2';

export function encodeComponentCustomId(
  source: ComponentV2Source,
  ref: string,
  actionId: string
): string {
  const customId = `${CUSTOM_ID_PREFIX}:${source}:${ref}:${actionId}`;
  if (customId.length > 100) {
    throw new Error(`custom_id exceeds 100 characters (${customId.length})`);
  }
  return customId;
}

export function decodeComponentCustomId(customId: string): {
  source: ComponentV2Source;
  ref: string;
  actionId: string;
} | null {
  if (!customId.startsWith(`${CUSTOM_ID_PREFIX}:`)) {
    return null;
  }
  const parts = customId.split(':');
  if (parts.length < 4) {
    return null;
  }
  const source = ComponentV2SourceSchema.safeParse(parts[1]);
  if (!source.success) {
    return null;
  }
  const actionId = parts[parts.length - 1]!;
  const ref = parts.slice(2, -1).join(':');
  if (!ref || !actionId) {
    return null;
  }
  return { source: source.data, ref, actionId };
}

export function isComponentV2CustomId(customId: string): boolean {
  return decodeComponentCustomId(customId) !== null;
}

/** Empty payload helper for UI builders. */
export function emptyComponentsV2(): MessageComponentsV2 {
  return {
    components: [
      {
        type: 'container',
        components: [{ type: 'text_display', content: 'Hello from Nexumi!' }]
      }
    ],
    actions: {}
  };
}

export function parseMessageComponentsV2(raw: unknown): MessageComponentsV2 | null {
  const parsed = MessageComponentsV2Schema.safeParse(raw);
  return parsed.success ? parsed.data : null;
}
