import * as v from 'valibot';

import { pick } from '../utils';
import { isDefaultAvatar, isDefaultHeader } from '../utils/accounts';
import { guessFqn } from '../utils/domain';

import { customEmojiSchema } from './custom-emoji';
import { relationshipSchema } from './relationship';
import { roleSchema } from './role';
import { coerceObject, datetimeSchema, filteredArray } from './utils';

const filterBadges = (tags?: string[]) =>
  tags
    ?.filter((tag) => tag.startsWith('badge:'))
    .map((tag) => v.parse(roleSchema, { id: tag, name: tag.replace(/^badge:/, '') }));

const MKLJCZK_ACCOUNTS = [
  'https://pl.fediverse.pl/users/mkljczk',
  'https://gts.mkljczk.pl/users/mkljczk',
  'https://gts.mkljczk.pl/@mkljczk',
];

const paymentOptionSchema = v.variant('type', [
  v.object({
    /** Payment type */
    type: v.literal('link'),
    /** Link name (only for link type) */
    name: v.fallback(v.nullable(v.string()), null),
    /** Link URL (only for link type) */
    href: v.fallback(v.nullable(v.string()), null),
    /** Unique identifier of a proposal object */
    object_id: v.fallback(v.nullable(v.string()), null),
  }),
  v.object({
    /** Payment type */
    type: v.literal('monero-subscription'),
    /** CAIP-2 chain ID (only for monero-subscription type) */
    chain_id: v.fallback(v.nullable(v.string()), null),
    /** Subscription price (only for monero-subscription type) */
    price: v.fallback(v.nullable(v.number()), null),
    /** Minimum payment amount (only for monero-subscription type) */
    amount_min: v.fallback(v.nullable(v.number()), null),
    /** Unique identifier of a proposal object */
    object_id: v.fallback(v.nullable(v.string()), null),
  }),
]);

const preprocessAccount = v.transform((account: any) => {
  if (!account?.acct) return null;

  const username = account.username || account.acct.split('@')[0];

  const fqn = account.fqn || guessFqn(account);
  const domain = fqn.split('@')[1] || '';

  const isCat =
    (account.pleroma?.is_cat ?? account.is_cat) ||
    MKLJCZK_ACCOUNTS.includes(account.uri ?? account.url);
  const speakAsCat = account.pleroma?.speak_as_cat ?? account.speak_as_cat ?? isCat;

  return {
    username,
    fqn,
    domain,
    avatar: account.avatar || account.avatar_static,
    header: account.header || account.header_static,
    avatar_default: isDefaultAvatar(account.avatar || account.avatar_static || ''),
    header_default: isDefaultHeader(account.header || account.header_static || ''),
    local:
      typeof account.pleroma?.is_local === 'boolean'
        ? account.pleroma.is_local
        : account.acct.split('@')[1] === undefined,
    discoverable: account.discoverable || account.pleroma?.source?.discoverable,
    verified: account.verified || account.pleroma?.tags?.includes('verified'),
    ...(account.role?.permissions
      ? {
          is_admin: (account.role?.permissions & 0x1) === 0x1,
        }
      : {}),
    ap_id: account.pleroma?.ap_id ?? account.actor_id,
    ...pick(account.pleroma || {}, [
      'background_image',
      'relationship',
      'is_moderator',
      'is_admin',
      'hide_favorites',
      'hide_followers',
      'hide_follows',
      'hide_followers_count',
      'hide_follows_count',
      'accepts_chat_messages',
      'favicon',
      'birthday',
      'deactivated',
      'avatar_description',
      'header_description',

      'settings_store',
      'chat_token',
      'allow_following_move',
      'unread_conversation_count',
      'unread_notifications_count',
      'notification_settings',

      'location',
    ]),
    ...pick(account.akkoma || {}, ['permit_followback']),
    is_cat: isCat,
    speak_as_cat: speakAsCat,
    ...pick(account.other_settings || {}, ['birthday', 'location']),
    __meta: pick(account, ['pleroma', 'source']),
    ...account,
    display_name:
      account.display_name
        ?.replace(
          /^[\s\u180E\u200B-\u200D\u2060\uFEFF]+|[\s\u180E\u200B-\u200D\u2060\uFEFF]+$/g,
          '',
        )
        .trim() || username,
    roles: account.roles?.length ? account.roles : filterBadges(account.pleroma?.tags),
    source: account.source
      ? {
          ...pick(account.pleroma?.source || {}, [
            'show_role',
            'no_rich_text',
            'discoverable',
            'actor_type',
            'show_birthday',
          ]),
          ...account.source,
        }
      : undefined,
  };
});

const fieldSchema = v.object({
  name: v.string(),
  value: v.string(),
  verified_at: v.fallback(v.nullable(datetimeSchema), null),
});

const baseAccountSchema = v.object({
  id: v.string(),
  username: v.fallback(v.string(), ''),
  acct: v.fallback(v.string(), ''),
  url: v.pipe(v.string(), v.url()),
  display_name: v.fallback(v.string(), ''),
  note: v.fallback(
    v.pipe(
      v.string(),
      v.transform((note) => (note === '<p></p>' ? '' : note)),
    ),
    '',
  ),
  avatar: v.fallback(v.string(), ''),
  avatar_static: v.fallback(v.pipe(v.string(), v.url()), ''),
  header: v.fallback(v.pipe(v.string(), v.url()), ''),
  header_static: v.fallback(v.pipe(v.string(), v.url()), ''),
  locked: v.fallback(v.boolean(), false),
  fields: filteredArray(fieldSchema),
  emojis: filteredArray(customEmojiSchema),
  bot: v.fallback(v.boolean(), false),
  group: v.fallback(v.boolean(), false),
  discoverable: v.fallback(v.boolean(), false),
  indexable: v.fallback(v.nullable(v.boolean()), null),
  noindex: v.fallback(v.nullable(v.boolean()), null),
  memorial: v.fallback(v.nullable(v.boolean()), null),
  suspended: v.fallback(v.optional(v.boolean()), undefined),
  limited: v.fallback(v.optional(v.boolean()), undefined),
  created_at: v.fallback(datetimeSchema, new Date().toISOString()),
  last_status_at: v.fallback(v.nullable(v.pipe(v.string(), v.isoDate())), null),
  statuses_count: v.fallback(v.number(), 0),
  followers_count: v.fallback(v.number(), 0),
  following_count: v.fallback(v.number(), 0),
  roles: filteredArray(roleSchema),
  hide_collections: v.fallback(v.optional(v.boolean()), undefined),

  fqn: v.string(),
  ap_id: v.fallback(v.nullable(v.string()), null),
  background_image: v.fallback(v.nullable(v.string()), null),
  relationship: v.fallback(v.optional(relationshipSchema), undefined),
  is_moderator: v.fallback(v.optional(v.boolean()), undefined),
  is_admin: v.fallback(v.optional(v.boolean()), undefined),
  is_suggested: v.fallback(v.optional(v.boolean()), undefined),
  hide_favorites: v.fallback(v.boolean(), true),
  hide_followers: v.fallback(v.optional(v.boolean()), undefined),
  hide_follows: v.fallback(v.optional(v.boolean()), undefined),
  hide_followers_count: v.fallback(v.optional(v.boolean()), undefined),
  hide_follows_count: v.fallback(v.optional(v.boolean()), undefined),
  accepts_chat_messages: v.fallback(v.nullable(v.boolean()), null),
  favicon: v.fallback(v.optional(v.string()), undefined),
  birthday: v.fallback(v.optional(v.pipe(v.string(), v.isoDate())), undefined),
  deactivated: v.fallback(v.optional(v.boolean()), undefined),

  location: v.fallback(v.optional(v.string()), undefined),
  local: v.fallback(v.optional(v.boolean()), false),

  permit_followback: v.fallback(v.optional(v.boolean()), undefined),

  avatar_description: v.fallback(v.string(), ''),
  custom_css: v.fallback(v.string(), ''),
  enable_rss: v.fallback(v.boolean(), false),
  header_description: v.fallback(v.string(), ''),

  verified: v.fallback(v.optional(v.boolean()), undefined),
  domain: v.fallback(v.string(), ''),

  pronouns: v.fallback(v.array(v.string()), []),

  /** Mention policy */
  mention_policy: v.fallback(v.picklist(['none', 'only_known', 'only_contacts']), 'none'),
  /** The reported subscribers of this user */
  subscribers_count: v.fallback(v.number(), 0),
  /** Identity proofs */
  identity_proofs: filteredArray(
    v.object({
      /** The key of a given field's key-value pair */
      name: v.fallback(v.string(), ''),
      /** The value associated with the name key */
      value: v.fallback(v.string(), ''),
      /** Timestamp of when the server verified the field value */
      verified_at: v.fallback(datetimeSchema, new Date().toISOString()),
    }),
  ),
  /** Payment options */
  payment_options: filteredArray(paymentOptionSchema),

  /** Whether the user is a cat */
  is_cat: v.fallback(v.boolean(), false),
  /** Whether the user's posts should be nyanified */
  speak_as_cat: v.fallback(v.boolean(), false),

  __meta: coerceObject({
    pleroma: v.optional(v.any(), undefined),
    source: v.optional(v.any(), undefined),
  }),

  avatar_default: v.fallback(v.boolean(), false),
  header_default: v.fallback(v.boolean(), false),
});

const accountWithMovedAccountSchema = v.object({
  ...baseAccountSchema.entries,
  moved: v.fallback(
    v.nullable(v.lazy((): typeof baseAccountSchema => accountWithMovedAccountSchema as any)),
    null,
  ),
});

/** @see {@link https://docs.joinmastodon.org/entities/Account/} */
const untypedAccountSchema = v.pipe(v.any(), preprocessAccount, accountWithMovedAccountSchema);

type WithMoved = {
  moved: Account | null;
};

/**
 * @category Entity types
 */
type Account = v.InferOutput<typeof accountWithMovedAccountSchema> & WithMoved;

/**
 * @category Schemas
 */
const accountSchema: v.BaseSchema<any, Account, v.BaseIssue<unknown>> = untypedAccountSchema as any;

const untypedCredentialAccountSchema = v.pipe(
  v.any(),
  preprocessAccount,
  v.object({
    ...accountWithMovedAccountSchema.entries,
    source: v.fallback(
      v.nullable(
        coerceObject({
          attribution_domains: v.fallback(v.optional(v.nullable(v.array(v.string()))), null),
          note: v.fallback(v.optional(v.string()), ''),
          fields: v.fallback(v.optional(filteredArray(fieldSchema)), []),
          privacy: v.fallback(
            v.optional(v.picklist(['public', 'unlisted', 'private', 'direct'])),
            'public',
          ),
          sensitive: v.fallback(v.optional(v.boolean()), false),
          language: v.fallback(v.optional(v.nullable(v.string())), null),
          follow_requests_count: v.fallback(
            v.optional(v.pipe(v.number(), v.integer(), v.minValue(0))),
            0,
          ),
          hide_collections: v.fallback(v.optional(v.boolean()), undefined),
          discoverable: v.fallback(v.optional(v.boolean()), undefined),
          indexable: v.fallback(v.nullable(v.boolean()), null),
          quote_policy: v.fallback(v.nullable(v.picklist(['public', 'followers', 'nobody'])), null),

          show_role: v.fallback(v.optional(v.nullable(v.boolean())), undefined),
          no_rich_text: v.fallback(v.optional(v.nullable(v.boolean())), undefined),
          actor_type: v.fallback(v.optional(v.string()), undefined),
          show_birthday: v.fallback(v.optional(v.boolean()), undefined),

          also_known_as_uris: v.fallback(v.optional(v.array(v.string())), undefined),
          status_content_type: v.fallback(v.optional(v.string()), undefined),
          web_include_boosts: v.fallback(v.optional(v.boolean()), undefined),
          web_layout: v.fallback(v.optional(v.picklist(['microblog', 'gallery'])), undefined),
          web_visibility: v.fallback(
            v.optional(v.picklist(['public', 'unlisted', 'none'])),
            undefined,
          ),
        }),
      ),
      null,
    ),
    role: v.fallback(v.nullable(roleSchema), null),

    settings_store: v.fallback(v.optional(v.record(v.string(), v.any())), undefined),
    chat_token: v.fallback(v.optional(v.string()), undefined),
    allow_following_move: v.fallback(v.optional(v.boolean()), undefined),
    unread_conversation_count: v.fallback(v.optional(v.number()), undefined),
    unread_notifications_count: v.fallback(v.optional(v.number()), undefined),
    notification_settings: v.fallback(
      v.optional(
        v.object({
          block_from_strangers: v.fallback(v.boolean(), false),
          hide_notification_contents: v.fallback(v.boolean(), false),
        }),
      ),
      undefined,
    ),
  }),
);

/**
 * @category Entity types
 */
type CredentialAccount = v.InferOutput<typeof untypedCredentialAccountSchema> & WithMoved;

/**
 * @category Schemas
 */
const credentialAccountSchema: v.BaseSchema<
  any,
  CredentialAccount,
  v.BaseIssue<unknown>
> = untypedCredentialAccountSchema as any;

const untypedBlockedAccountSchema = v.pipe(
  v.any(),
  preprocessAccount,
  v.object({
    ...accountWithMovedAccountSchema.entries,
    block_expires_at: v.fallback(v.nullable(datetimeSchema), null),
  }),
);

/**
 * @category Entity types
 */
type BlockedAccount = v.InferOutput<typeof untypedBlockedAccountSchema> & WithMoved;

/**
 * @category Schemas
 */
const blockedAccountSchema: v.BaseSchema<
  any,
  BlockedAccount,
  v.BaseIssue<unknown>
> = untypedBlockedAccountSchema as any;

const untypedMutedAccountSchema = v.pipe(
  v.any(),
  preprocessAccount,
  v.object({
    ...accountWithMovedAccountSchema.entries,
    mute_expires_at: v.fallback(v.nullable(datetimeSchema), null),
  }),
);

/**
 * @category Entity types
 */
type MutedAccount = v.InferOutput<typeof untypedMutedAccountSchema> & WithMoved;

/**
 * @category Schemas
 */
const mutedAccountSchema: v.BaseSchema<
  any,
  MutedAccount,
  v.BaseIssue<unknown>
> = untypedMutedAccountSchema as any;

export {
  accountSchema,
  credentialAccountSchema,
  blockedAccountSchema,
  mutedAccountSchema,
  type Account,
  type CredentialAccount,
  type BlockedAccount,
  type MutedAccount,
};
