import * as v from 'valibot';

import { accountSchema } from './account';
import { ruleSchema } from './rule';
import { coerceObject, filteredArray, mimeSchema } from './utils';

const SNAC_REGEX = /^([0-9.]*) \(not true; really snac\/([\w.]*)\)/;
const WORDPRESS_REGEX = /^WordPress\/[\w+.-]*, EMA\/([\w+.-]*)/;

const getApiVersions = (instance: any): Record<string, number> => ({
  ...Object.fromEntries(
    instance.pleroma?.metadata?.features?.map((feature: string) => {
      let string = `${feature}.pleroma.pl-api`;
      if (string.startsWith('pleroma:') || string.startsWith('pleroma_')) string = string.slice(8);
      if (string.startsWith('akkoma:')) string = string.slice(7);
      if (string.startsWith('pl:')) string = string.slice(3);
      return [string, 1];
    }) || [],
  ),
  ...Object.fromEntries(
    instance.fedibird_capabilities?.map((feature: string) => [`${feature}.fedibird.pl-api`, 1]) ||
      [],
  ),
  ...instance.api_versions,
});

const instanceV1ToV2 = (data: any) => {
  const {
    approval_required,
    configuration,
    contact_account,
    description,
    description_limit,
    email,
    max_media_attachments,
    max_toot_chars,
    poll_limits,
    pleroma,
    registrations,
    short_description,
    thumbnail,
    upload_limit,
    uri,
    urls,
    ...instance
  } = v.parse(instanceV1Schema, data);

  let domain = uri;
  try {
    domain = new URL(uri).host;
  } catch {
    // Ignore invalid URL
  }

  return {
    ...instance,
    account_domain: instance.account_domain || uri,
    configuration: {
      ...configuration,
      media_attachments: {
        ...configuration.media_attachments,
        image_size_limit: upload_limit ?? configuration.media_attachments.image_size_limit,
        video_size_limit: upload_limit ?? configuration.media_attachments.video_size_limit,
      },
      polls: {
        ...configuration.polls,
        max_characters_per_option:
          poll_limits.max_option_chars ?? configuration.polls.max_characters_per_option,
        max_expiration: poll_limits.max_expiration ?? configuration.polls.max_expiration,
        max_options: poll_limits.max_options ?? configuration.polls.max_options,
        min_expiration: poll_limits.min_expiration ?? configuration.polls.min_expiration,
      },
      statuses: {
        ...configuration.statuses,
        max_characters: max_toot_chars ?? configuration.statuses.max_characters,
        max_media_attachments:
          max_media_attachments ?? configuration.statuses.max_media_attachments,
      },
      urls: {
        streaming: urls.streaming_api,
      },
      vapid: {
        public_key: pleroma.vapid_public_key,
      },
    },
    contact: {
      account: contact_account,
      email: email,
    },
    description: short_description || description,
    domain,
    pleroma: {
      ...pleroma,
      metadata: {
        ...pleroma.metadata,
        description_limit,
      },
    },
    registrations: {
      approval_required: approval_required,
      enabled: registrations,
    },
    thumbnail: { url: thumbnail },
  };
};

const software: Array<[string, string]> = [
  ['takahe', 'Takahe'],
  ['neodb', 'NeoDB'],
  ['egregoros', 'Egregoros'],
];

const fixVersion = (version: string) => {
  // Handle Mastodon release candidates
  if (new RegExp(/[0-9.]+rc[0-9]+/g).test(version)) {
    return version.split('rc').join('-rc');
  }

  // Set Takahē and NeoDB versions to a Pleroma-like string
  for (const [key, name] of software) {
    if (version.startsWith(`${key}/`)) {
      return `0.0.0 (compatible; ${name} ${version.slice(key.length + 1)})`;
    }
  }

  const wordPressMatch = WORDPRESS_REGEX.exec(version);

  if (wordPressMatch) {
    return `0.0.0 (compatible; WordPress ${wordPressMatch[1]})`;
  }

  const snacMatch = SNAC_REGEX.exec(version);

  if (snacMatch) {
    return `${snacMatch[1]} (compatible; snac ${snacMatch[2]})`;
  }

  return version;
};

const configurationSchema = coerceObject({
  accounts: v.fallback(
    v.nullable(
      v.object({
        allow_custom_css: v.boolean(),
        max_featured_tags: v.pipe(v.number(), v.integer()),
        max_profile_fields: v.pipe(v.number(), v.integer()),
      }),
    ),
    null,
  ),
  chats: coerceObject({
    max_characters: v.fallback(v.number(), 5000),
  }),
  groups: coerceObject({
    max_characters_description: v.fallback(v.number(), 160),
    max_characters_name: v.fallback(v.number(), 50),
  }),
  media_attachments: coerceObject({
    image_matrix_limit: v.fallback(v.optional(v.number()), undefined),
    image_size_limit: v.fallback(v.optional(v.number()), undefined),
    supported_mime_types: v.fallback(v.optional(v.array(mimeSchema)), undefined),
    video_duration_limit: v.fallback(v.optional(v.number()), undefined),
    video_frame_rate_limit: v.fallback(v.optional(v.number()), undefined),
    video_matrix_limit: v.fallback(v.optional(v.number()), undefined),
    video_size_limit: v.fallback(v.optional(v.number()), undefined),
  }),
  polls: coerceObject({
    max_characters_per_option: v.fallback(v.number(), 25),
    max_expiration: v.fallback(v.number(), 2629746),
    max_options: v.fallback(v.number(), 4),
    min_expiration: v.fallback(v.number(), 300),
  }),
  reactions: coerceObject({
    max_reactions: v.fallback(v.number(), 0),
  }),
  statuses: coerceObject({
    characters_reserved_per_url: v.fallback(v.optional(v.number()), undefined),
    max_characters: v.fallback(v.number(), 500),
    max_media_attachments: v.fallback(v.number(), 4),
  }),
  translation: coerceObject({
    enabled: v.fallback(v.boolean(), false),
  }),
  timelines_access: coerceObject({
    ...v.entriesFromList(
      ['hashtag_feeds', 'trending_link_feeds'],
      coerceObject(
        v.entriesFromList(
          ['local', 'remote'],
          v.fallback(v.picklist(['public', 'authenticated', 'disabled', 'restricted']), 'public'),
        ),
      ),
    ),
    live_feeds: coerceObject(
      v.entriesFromList(
        ['local', 'bubble', 'remote', 'wrenched'],
        v.fallback(v.picklist(['public', 'authenticated', 'disabled']), 'public'),
      ),
    ),
  }),
  urls: coerceObject({
    streaming: v.fallback(v.optional(v.pipe(v.string(), v.url())), undefined),
    status: v.fallback(v.optional(v.pipe(v.string(), v.url())), undefined),
    about: v.fallback(v.nullable(v.string()), null),
    privacy_policy: v.fallback(v.nullable(v.string()), null),
    terms_of_service: v.fallback(v.nullable(v.string()), null),
  }),
  vapid: coerceObject({
    public_key: v.fallback(v.string(), ''),
  }),
  limited_federation: v.optional(v.boolean(), undefined),
});

const contactSchema = coerceObject({
  contact_account: v.fallback(v.optional(accountSchema), undefined),
  email: v.fallback(v.pipe(v.string(), v.email()), ''),
});

const pleromaSchema = coerceObject({
  metadata: coerceObject({
    account_activation_required: v.fallback(v.boolean(), false),
    birthday_min_age: v.fallback(v.number(), 0),
    birthday_required: v.fallback(v.boolean(), false),
    description_limit: v.fallback(v.number(), 1500),
    federation: coerceObject({
      enabled: v.fallback(v.boolean(), true), // Assume true unless explicitly false
      mrf_policies: v.fallback(v.optional(v.array(v.string())), undefined),
      mrf_simple: coerceObject(
        v.entriesFromList(
          [
            'accept',
            'avatar_removal',
            'banner_removal',
            'federated_timeline_removal',
            'followers_only',
            'media_nsfw',
            'media_removal',
            'reject',
            'reject_deletes',
            'report_removal',
          ],
          v.fallback(v.array(v.string()), []),
        ),
      ),
      mrf_simple_info: coerceObject(
        v.entriesFromList(
          [
            'accept',
            'avatar_removal',
            'banner_removal',
            'federated_timeline_removal',
            'followers_only',
            'media_nsfw',
            'media_removal',
            'reject',
            'reject_deletes',
            'report_removal',
          ],
          v.fallback(v.array(v.tuple([v.string(), v.string()])), []),
        ),
      ),
    }),
    fields_limits: coerceObject({
      max_fields: v.fallback(v.pipe(v.number(), v.integer(), v.minValue(0)), 4),
      name_length: v.fallback(v.pipe(v.number(), v.integer(), v.minValue(0)), 255),
      value_length: v.fallback(v.pipe(v.number(), v.integer(), v.minValue(0)), 2047),
    }),
    markup: coerceObject({
      allow_headings: v.fallback(v.boolean(), false),
      allow_inline_images: v.fallback(v.boolean(), false),
    }),
    migration_cooldown_period: v.fallback(v.optional(v.number()), undefined),
    multitenancy: coerceObject({
      domains: v.optional(
        v.array(
          v.object({
            domain: v.pipe(v.unknown(), v.transform(String)),
            id: v.string(),
            public: v.fallback(v.boolean(), false),
          }),
        ),
      ),
      enabled: v.fallback(v.boolean(), false),
    }),
    post_formats: v.fallback(v.array(v.string()), ['text/plain']),
    restrict_unauthenticated: coerceObject({
      activities: coerceObject({
        local: v.fallback(v.boolean(), false),
        remote: v.fallback(v.boolean(), false),
      }),
      profiles: coerceObject({
        local: v.fallback(v.boolean(), false),
        remote: v.fallback(v.boolean(), false),
      }),
      timelines: coerceObject({
        bubble: v.fallback(v.boolean(), false),
        federated: v.fallback(v.boolean(), false),
        local: v.fallback(v.boolean(), false),
        wrenched: v.fallback(v.boolean(), false),
      }),
    }),
    translation: coerceObject({
      allow_remote: v.fallback(v.boolean(), true),
      allow_unauthenticated: v.fallback(v.boolean(), false),
      source_languages: v.fallback(v.optional(v.array(v.string())), undefined),
      target_languages: v.fallback(v.optional(v.array(v.string())), undefined),
    }),
  }),
  oauth_consumer_strategies: v.fallback(v.array(v.string()), []),
  stats: coerceObject({
    mau: v.fallback(v.optional(v.number()), undefined),
  }),
  vapid_public_key: v.fallback(v.string(), ''),
});

const pleromaPollLimitsSchema = coerceObject({
  max_expiration: v.fallback(v.optional(v.number()), undefined),
  max_option_chars: v.fallback(v.optional(v.number()), undefined),
  max_options: v.fallback(v.optional(v.number()), undefined),
  min_expiration: v.fallback(v.optional(v.number()), undefined),
});

const registrations = coerceObject({
  approval_required: v.fallback(v.boolean(), false),
  enabled: v.fallback(v.boolean(), false),
  message: v.fallback(v.optional(v.string()), undefined),
  min_age: v.fallback(v.nullable(v.number()), null),
  reason_required: v.fallback(v.nullable(v.boolean()), null),
  url: v.fallback(v.nullable(v.pipe(v.string(), v.url())), null),
});

const statsSchema = coerceObject({
  domain_count: v.fallback(v.optional(v.number()), undefined),
  status_count: v.fallback(v.optional(v.number()), undefined),
  user_count: v.fallback(v.optional(v.number()), undefined),
});

const thumbnailSchema = coerceObject({
  url: v.fallback(v.string(), ''),
});

const usageSchema = coerceObject({
  users: coerceObject({
    active_month: v.fallback(v.number(), 0),
  }),
});

const instanceV1Schema = coerceObject({
  account_domain: v.fallback(v.string(), ''),
  approval_required: v.fallback(v.boolean(), false),
  configuration: configurationSchema,
  contact_account: v.fallback(v.optional(accountSchema), undefined),
  description: v.fallback(v.string(), ''),
  description_limit: v.fallback(v.number(), 1500),
  email: v.fallback(v.pipe(v.string(), v.email()), ''),
  feature_quote: v.fallback(v.boolean(), false),
  languages: v.fallback(v.array(v.string()), []),
  max_media_attachments: v.fallback(v.optional(v.number()), undefined),
  max_toot_chars: v.fallback(v.optional(v.number()), undefined),
  pleroma: pleromaSchema,
  poll_limits: pleromaPollLimitsSchema,
  registrations: v.fallback(v.boolean(), false),
  rules: filteredArray(ruleSchema),
  short_description: v.fallback(v.string(), ''),
  stats: statsSchema,
  thumbnail: v.fallback(v.string(), ''),
  title: v.fallback(v.string(), ''),
  upload_limit: v.fallback(v.optional(v.number()), undefined),
  uri: v.fallback(v.string(), ''),
  urls: coerceObject({
    streaming_api: v.fallback(v.optional(v.pipe(v.string(), v.url())), undefined),
  }),
  usage: usageSchema,
  version: v.fallback(v.string(), '0.0.0'),
});

/**
 * @category Schemas
 * @see {@link https://docs.joinmastodon.org/entities/Instance/}
 */
const instanceSchema = v.pipe(
  v.any(),
  v.transform((data: any) => {
    // Detect Hollo
    if (data.source_url === 'https://github.com/fedify-dev/hollo') {
      data.version = `0.0.0 (compatible; Hollo ${data.version})`;
    }

    // Detect GoToSocial
    if (typeof data.configuration?.accounts?.allow_custom_css === 'boolean') {
      data.version = `0.0.0 (compatible; GoToSocial ${data.version})`;
    }

    const apiVersions = getApiVersions(data);

    if (!data.pleroma) {
      data.pleroma = {
        metadata: {
          post_formats:
            data.configuration?.statuses?.supported_mime_types ||
            (apiVersions['kmyblue_markdown.fedibird.pl-api']
              ? ['text/plain', 'text/markdown']
              : []),
        },
      };
    }

    if (data.pleroma?.metadata?.restrict_unauthenticated?.timelines) {
      const timelines = data.pleroma.metadata.restrict_unauthenticated.timelines;
      const features = data.pleroma.metadata.features || [];

      if (!data.configuration) data.configuration = {};

      data.configuration.timelines_access = {
        ...data.configuration.timelines_access,
        live_feeds: {
          bubble: timelines.bubble
            ? 'authenticated'
            : features.includes('bubble_timeline')
              ? 'public'
              : 'disabled',
          local: timelines.local ? 'authenticated' : 'public',
          remote: timelines.federated ? 'authenticated' : 'public',
          wrenched: timelines.wrenched
            ? 'authenticated'
            : features.includes('pleroma:wrenched_timeline')
              ? 'public'
              : 'disabled',
        },
      };
    }

    if (data.domain) return { account_domain: data.domain, ...data, api_versions: apiVersions };

    return { ...instanceV1ToV2(data), api_versions: apiVersions };
  }),
  coerceObject({
    account_domain: v.fallback(v.string(), ''),
    api_versions: v.fallback(v.record(v.string(), v.number()), {}),
    configuration: configurationSchema,
    contact: contactSchema,
    description: v.fallback(v.string(), ''),
    domain: v.fallback(v.string(), ''),
    feature_quote: v.fallback(v.boolean(), false),
    icons: filteredArray(
      v.object({
        size: v.pipe(v.string(), v.regex(/^[0-9]+x[0-9]+$/)),
        src: v.string(),
      }),
    ),
    languages: v.fallback(v.array(v.string()), []),
    pleroma: pleromaSchema,
    registrations,
    rules: filteredArray(ruleSchema),
    stats: statsSchema,
    thumbnail: thumbnailSchema,
    title: v.fallback(v.string(), ''),
    usage: usageSchema,
    version: v.pipe(v.fallback(v.string(), '0.0.0'), v.transform(fixVersion)),
    blockchains: v.fallback(
      v.optional(
        filteredArray(
          v.object({
            chain_id: v.fallback(v.nullable(v.string()), null),
            chain_metadata: coerceObject({
              is_forwarding_required: v.fallback(v.boolean(), false),
              description: v.fallback(v.string(), ''),
              payment_amount_min: v.number(),
            }),
            features: coerceObject({
              subscriptions: v.fallback(v.boolean(), false),
            }),
          }),
        ),
      ),
      undefined,
    ),
  }),
);

/**
 * @category Entity types
 */
type Instance = v.InferOutput<typeof instanceSchema>;

export { instanceSchema, type Instance };
