content: /*
   * Kiwii, a stupid Discord bot.
   * Copyright (C) 2019-2024 Lexedia
   * 
   * This program is free software: you can redistribute it and/or modify
   * it under the terms of the GNU General Public License as published by
   * the Free Software Foundation, either version 3 of the License, or
   * (at your option) any later version.
   * 
   * This program is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU General Public License for more details.
   * 
   * You should have received a copy of the GNU General Public License
   * along with this program.  If not, see <https://www.gnu.org/licenses/>.
   */
  
  import 'dart:math';
  
  import 'package:get_it/get_it.dart';
  import 'package:nyxx/nyxx.dart';
  import 'package:nyxx_extensions/nyxx_extensions.dart';
  import 'package:sentry/sentry.dart';
  
  import '../../../kiwii.dart';
  import '../../../translations.g.dart';
  import '../../models/appeal.dart';
  import '../../models/case.dart';
  import '../appeal/create_appeal.dart';
  import '../case/create_case.dart';
  import 'strings.dart';
  
  enum HistoryType {
    // ignore: constant_identifier_names
    case_,
    report,
  }
  
  enum Colours {
    lime._(0x7ef31f),
    green._(0x80f31f),
    greenYellow._(0xa5de0b),
    yellow._(0xc7c101),
    orangeYellow._(0xe39e03),
    orange._(0xf6780f),
    red._(0xfe5326),
    crimson._(0xfb3244),
    unknown._(0x5865f2);
  
    final int value;
  
    const Colours._(this.value);
  }
  
  typedef HistoryRecord = ({DateTime createdAt, String identifierLabel, Uri identifierUri, String label, String? description});
  
  Future<EmbedBuilder> generateHistory(({Member? member, User user}) target, Snowflake guildId, Translations t, [HistoryType type = HistoryType.case_]) async {
    final embed = generateUserInfo(target, t);
  
    return switch (type) {
      HistoryType.case_ => mergeEmbeds(await generateCaseHistory(target, guildId, t), embed),
      _ => embed,
    };
  }
  
  Future<EmbedBuilder> generateCaseHistory(({Member? member, User user}) target, Snowflake guildId, Translations t) async {
    final client = target.user.manager.client;
  
    final guildSettings = await client.repositories.guilds.get(guildId);
    final cases = await client.repositories.connection.execute(
      r'SELECT * FROM cases WHERE guild_id = $1 AND target_id = $2 AND action NOT IN (1, 8) ORDER BY created_at DESC;',
      parameters: [guildId.value, target.user.id.value]
    ).then((r) => r.map((s) => Case.fromRow(s.toColumnMap())));
  
    final caseFolded = cases.fold<Map<CaseAction, int>>({}, (acc, c) {
      final action = c.action;
      acc.update(action, (value) => value + 1, ifAbsent: () => 1);
      return acc;
    });
  
    final values = [
      caseFolded[CaseAction.role] ?? 0,
      caseFolded[CaseAction.warn] ?? 0,
      caseFolded[CaseAction.kick] ?? 0,
      caseFolded[CaseAction.softBan] ?? 0,
      caseFolded[CaseAction.ban] ?? 0,
      caseFolded[CaseAction.unban] ?? 0,
      caseFolded[CaseAction.timeout] ?? 0,
    ];
  
    final coloursIndex = min(values.reduce((a, b) => a + b), Colours.values.length - 1);
  
    final records = cases.map<HistoryRecord>(
      (case_) => (
        createdAt: case_.createdAt,
        identifierLabel: '#${case_.caseId}',
        identifierUri: Uri.https('discord.com', '/channels/$guildId/${guildSettings.modLogChannelId}/${case_.logMessageId}'),
        label: formatCaseAction(case_.action, t),
        description: case_.reason,
      ),
    );
  
    return generateHistoryEmbed(target.user, t.moderation.history.cases.title, Colours.values[coloursIndex].value, records, actionSummary(values, t), t);
  }
  
  String actionSummary(List<int> actions, Translations t) {
    final [restrictions, warns, kicks, softbans, bans, unbans, timeout] = actions;
  
    return [
      t.moderation.history.cases.summary.restriction(n: restrictions),
      t.moderation.history.cases.summary.warning(n: warns),
      t.moderation.history.cases.summary.kick(n: kicks),
      t.moderation.history.cases.summary.softban(n: softbans),
      t.moderation.history.cases.summary.ban(n: bans),
      t.moderation.history.cases.summary.unban(n: unbans),
      t.moderation.history.cases.summary.timeout(n: timeout),
    ].join(', ');
  }
  
  EmbedBuilder generateHistoryEmbed(User author, String title, int colour, Iterable<HistoryRecord> records, String footerText, Translations t) {
    return EmbedBuilder(
      description: <String>[
        if (records.isEmpty)
          t.moderation.common.errors.noHistory
        else
          for (final record in records)
            '${formatDate(record.createdAt, TimestampStyle.shortDate)} ${inlineCode(record.label)} ${hyperlink(record.identifierLabel, record.identifierUri.toString())} ${record.description ?? ''}',
      ].join('\n'),
      title: title,
      color: DiscordColor(colour),
      footer: EmbedFooterBuilder(text: footerText),
      author: EmbedAuthorBuilder(name: '${author.tag} (${author.id})', iconUrl: author.avatar.url),
    );
  }
  
  EmbedBuilder generateUserInfo(({Member? member, User user}) target, Translations t) {
    final since = formatDate(target.user.id.timestamp, TimestampStyle.relativeTime);
    final createdAt = formatDate(target.user.id.timestamp, TimestampStyle.shortDateTime);
  
    final embed = EmbedBuilder(
      author: EmbedAuthorBuilder(name: '${target.user.tag} (${target.user.id})', iconUrl: target.user.avatar.url),
      color: DiscordColor(0x5865f2),
      fields: [
        EmbedFieldBuilder(
          name: t.moderation.history.common.userDetails.title,
          value: t.moderation.history.common.userDetails.description(
            mention: target.user.mention,
            tag: target.user.tag,
            id: target.user.id,
            created: createdAt,
            createdSince: since,
            createdAtTimestamp: target.user.id.timestamp.millisecondsSinceEpoch,
          ),
          isInline: false,
        ),
      ],
    );
  
    if (target.member != null) {
      final joinedAt = formatDate(target.member!.joinedAt, TimestampStyle.shortDateTime);
      final joinedSince = formatDate(target.member!.joinedAt, TimestampStyle.relativeTime);
  
      final nonDefaultRoles = target.member!.roles.where((role) => role.id != target.member?.manager.guildId);
  
      embed.addField(
        name: t.moderation.history.common.memberDetails.title,
        value: [
          if (target.member?.nick != null) t.moderation.history.common.memberDetails.description.nickname(nickname: target.member!.nick!),
          if (nonDefaultRoles.isNotEmpty)
            t.moderation.history.common.memberDetails.description.roles(n: nonDefaultRoles.length, roles: nonDefaultRoles.map((r) => r.mention).join(', ')),
          t.moderation.history.common.memberDetails.description.joined(
            joined: joinedAt,
            joinedSince: joinedSince,
            joinedAtTimestamp: target.member!.joinedAt.millisecondsSinceEpoch,
          ),
        ].join('\n'),
        isInline: false,
      );
    }
  
    return embed;
  }
  
  Future<String> generateCaseLog(Case ccase, Snowflake logChannelId, Translations t, String prefix) async {
    final client = GetIt.I.get<NyxxGateway>();
    var action = formatCaseAction(ccase.action, t);
  
    if (ccase.action case CaseAction.role || CaseAction.unrole when ccase.roleId != null) {
      try {
        final guild = await client.guilds.get(ccase.guildId);
        final role = await guild.roles.get(ccase.roleId!);
  
        action += ' `@${role.name}` (${role.id})';
      } catch (e) {
        Sentry.logger.fmt.warn('Failed to fetch role for case %s', [ccase.caseId], attributes: {'exception': SentryLogAttribute.string(e.toString())});
      }
    }
  
    var msg = t.moderation.logs.cases.description(targetTag: ccase.targetTag, targetID: ccase.targetId, action: action);
  
    if (ccase.actionExpiration != null) {
      msg += t.moderation.logs.cases.expiration(time: formatDate(ccase.actionExpiration!, TimestampStyle.relativeTime));
    }
  
    if (ccase.contextMessageId != null) {
      // TODO: Fetch message
    }
  
    if (ccase.reason != null) {
      msg += t.moderation.logs.cases.reason(reason: ccase.reason!);
    } else {
      msg += t.moderation.logs.cases.reasonFallback(prefix: prefix, caseId: ccase.caseId);
    }
  
    if (ccase.refId != null) {
      final reference = await client.repositories.cases.get(ccase.refId!, ccase.guildId);
  
      msg += t.moderation.logs.cases.caseReference(
        action: formatCaseAction(reference.action, t),
        ref: hyperlink('#${ccase.refId}', 'https://discord.com/channels/${ccase.guildId}/$logChannelId/${reference.logMessageId}'),
      );
    }
  
    if (ccase.reportRefId != null) {
      // final report = 
    }
  
    return msg;
  }
  
  Future<EmbedBuilder> generateCaseEmbed(Snowflake guildId, Snowflake logChannelId, Case ccase, Translations t, String prefix, [User? user]) async {
    final embed = EmbedBuilder(
      color: DiscordColor(generateCaseColour(ccase.action).value),
      description: await generateCaseLog(ccase, logChannelId, t, prefix),
      footer: EmbedFooterBuilder(text: t.moderation.logs.cases.footer(caseId: ccase.caseId)),
      timestamp: ccase.createdAt.toUtc(),
      author: user != null ? EmbedAuthorBuilder(name: '${user.tag} (${user.id})', iconUrl: user.avatar.url) : null,
    );
  
    if (user != null) {
      embed.author = EmbedAuthorBuilder(name: '${user.tag} (${user.id})', iconUrl: user.avatar.url);
    }
  
    return embed;
  }
  
  Colours generateCaseColour(CaseAction action) => switch (action) {
    CaseAction.role || CaseAction.warn || CaseAction.timeout => Colours.yellow,
    CaseAction.kick || CaseAction.softBan => Colours.orangeYellow,
    CaseAction.ban => Colours.red,
    CaseAction.unban => Colours.green,
    _ => Colours.unknown,
  };
  
  Future<EmbedBuilder> generateAppealEmbed(Appeal appeal, User user, Translations t, [User? mod]) async {
    final footerText = switch (appeal.status) {
      AppealStatus.accepted => t.moderation.logs.appeals.footerAccepted(moderator: mod!.tag),
      AppealStatus.denied => t.moderation.logs.appeals.footerRejected(moderator: mod!.tag),
      _ => t.moderation.logs.appeals.footer,
    };
  
    final embed = EmbedBuilder(
      title: t.moderation.logs.appeals.title,
      description: t.moderation.logs.appeals.description(user: user.mention, caseId: appeal.refId!, reason: appeal.reason!, userID: user.id, userTag: user.tag),
      author: EmbedAuthorBuilder(name: user.tag, iconUrl: user.avatar.url),
      footer: EmbedFooterBuilder(text: footerText),
      color: DiscordColor(0x5865f2),
    );
  
    return embed;
  }
  
  Future<List<ActionRowBuilder>> generateAppealComponents(Appeal appeal, User user, Translations t) async {
    final List<ActionRowBuilder> components = switch (appeal.status) {
      AppealStatus.accepted => [],
      AppealStatus.denied => [],
      _ => [
        ActionRowBuilder(
          components: [
            ButtonBuilder(style: ButtonStyle.primary, label: t.moderation.buttons.appealApprove, customId: 'appeal-accept-${appeal.refId}-${user.id}'),
            ButtonBuilder(style: ButtonStyle.danger, label: t.moderation.buttons.appealReject, customId: 'appeal-reject-${appeal.refId}-${user.id}'),
          ],
        ),
      ],
    };
  
    return components;
  }
  
  EmbedBuilder generateMemberLog(Member member, User user, Translations t, {bool isJoin = false}) {
    var description =
        '${t.logs.guildLogs.memberLog.description(user: user.mention, userTag: user.tag, userId: user.id, createdAt: user.id.timestamp.format(TimestampStyle.shortDateTime), createdSince: user.id.timestamp.format(TimestampStyle.relativeTime))}\n';
  
    description += t.logs.guildLogs.memberLog.joinedAt(
      joinedAt: member.joinedAt.format(TimestampStyle.shortDateTime),
      joinedSince: member.joinedAt.format(TimestampStyle.relativeTime),
    );
  
    if (!isJoin) {
      description +=
          '\n${t.logs.guildLogs.memberLog.leftAt(leftAt: DateTime.now().format(TimestampStyle.shortDateTime), leftSince: DateTime.now().format(TimestampStyle.relativeTime))}';
    }
  
    return EmbedBuilder(
      author: EmbedAuthorBuilder(name: '${user.tag} (${user.id})', iconUrl: user.avatar.url),
      description: description,
      color: colourFromDuration(DateTime.now().difference(user.id.timestamp)),
      footer: EmbedFooterBuilder(text: isJoin ? t.logs.guildLogs.memberLog.footer.joined : t.logs.guildLogs.memberLog.footer.left),
    );
  }
  
  DiscordColor colourFromDuration(Duration duration) {
    final per = min(duration.inMilliseconds ~/ (Duration(days: 7 * 4).inMilliseconds ~/ 100), 100);
  
    int r, g, b = 0;
  
    if (per < 50) {
      r = 255;
      g = (5.1 * per).round();
    } else {
      g = 255;
      r = (5.1 * (100 - per)).round();
    }
  
    final tintFactor = .3;
    r += ((255 - r) * tintFactor).round();
    g += ((255 - g) * tintFactor).round();
    b += ((255 - b) * tintFactor).round();
  
    return DiscordColor.fromRgb(r, g, b);
  }
  