import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../shared/prisma/prisma.service';
import { appBadRequest, appNotFound } from '../../shared/common/app-error';

export type PlayerMessageType =
  | 'DEPOSIT_APPROVED'
  | 'DEPOSIT_REJECTED'
  | 'BANNER_PROMO'
  | 'ANNOUNCEMENT_PROMO'
  | 'ADMIN_CUSTOM';

export type DepositMessagePayload = {
  depositOrderId: string;
  orderNo: string;
  amount: string;
  approvedAmount?: string | null;
  rejectReason?: string | null;
};

export type BannerPromoPayload = {
  contentId: string;
};

type ContentTranslationLike = {
  locale: string;
  title?: string | null;
  body?: string | null;
};

const SUPPORTED_LOCALES = ['zh-CN', 'en-US', 'ms-MY'] as const;

const BANNER_PROMO_DEFAULT_TITLE: Record<string, string> = {
  'zh-CN': '新推广活动',
  'en-US': 'New promotion',
  'ms-MY': 'Promosi baharu',
};

const ANNOUNCEMENT_PROMO_DEFAULT_TITLE: Record<string, string> = {
  'zh-CN': '新公告',
  'en-US': 'New announcement',
  'ms-MY': 'Pengumuman baharu',
};

type MessageTemplate = {
  title: string;
  body: (payload: DepositMessagePayload) => string;
};

const MESSAGE_TEMPLATES: Record<
  Exclude<PlayerMessageType, 'BANNER_PROMO' | 'ANNOUNCEMENT_PROMO' | 'ADMIN_CUSTOM'>,
  Record<string, MessageTemplate>
> = {
  DEPOSIT_APPROVED: {
    'zh-CN': {
      title: '充值已到账',
      body: (p) =>
        `您的充值订单 ${p.orderNo} 已审核通过，申请金额 ${p.amount}，到账金额 ${p.approvedAmount ?? p.amount}。`,
    },
    'en-US': {
      title: 'Deposit approved',
      body: (p) =>
        `Your deposit order ${p.orderNo} has been approved. Requested ${p.amount}, credited ${p.approvedAmount ?? p.amount}.`,
    },
    'ms-MY': {
      title: 'Deposit diluluskan',
      body: (p) =>
        `Pesanan deposit ${p.orderNo} telah diluluskan. Diminta ${p.amount}, dikreditkan ${p.approvedAmount ?? p.amount}.`,
    },
  },
  DEPOSIT_REJECTED: {
    'zh-CN': {
      title: '充值未通过',
      body: (p) => {
        const reason = p.rejectReason?.trim();
        return reason
          ? `您的充值订单 ${p.orderNo}（${p.amount}）未通过审核。原因：${reason}`
          : `您的充值订单 ${p.orderNo}（${p.amount}）未通过审核。`;
      },
    },
    'en-US': {
      title: 'Deposit rejected',
      body: (p) => {
        const reason = p.rejectReason?.trim();
        return reason
          ? `Your deposit order ${p.orderNo} (${p.amount}) was rejected. Reason: ${reason}`
          : `Your deposit order ${p.orderNo} (${p.amount}) was rejected.`;
      },
    },
    'ms-MY': {
      title: 'Deposit ditolak',
      body: (p) => {
        const reason = p.rejectReason?.trim();
        return reason
          ? `Pesanan deposit ${p.orderNo} (${p.amount}) ditolak. Sebab: ${reason}`
          : `Pesanan deposit ${p.orderNo} (${p.amount}) ditolak.`;
      },
    },
  },
};

function resolveLocale(locale?: string | null): string {
  const value = locale?.trim();
  if (value && SUPPORTED_LOCALES.includes(value as (typeof SUPPORTED_LOCALES)[number])) {
    return value;
  }
  return 'en-US';
}

function pickContentTranslation<T extends { locale: string }>(
  translations: T[],
  locale: string,
): T | undefined {
  const chain = [locale, 'en-US', 'zh-CN', 'ms-MY'];
  for (const loc of chain) {
    const hit = translations.find((tr) => tr.locale === loc);
    if (hit) return hit;
  }
  return translations[0];
}

function stripHtml(value: string): string {
  return value.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
}

function buildBannerPromoFallbackBody(locale: string, title: string): string {
  if (locale === 'zh-CN') return `「${title}」已上线，请到首页查看详情。`;
  if (locale === 'ms-MY') return `"${title}" kini tersedia. Lihat butiran di halaman utama.`;
  return `"${title}" is now live. View details on the home page.`;
}

function buildAnnouncementPromoFallbackBody(locale: string, title: string): string {
  if (locale === 'zh-CN') return `公告「${title}」已发布，请及时查看。`;
  if (locale === 'ms-MY') return `Pengumuman "${title}" telah diterbitkan. Sila semak.`;
  return `Announcement "${title}" is published. Please check it out.`;
}

function buildBannerPromoMessage(
  locale: string | null | undefined,
  contentId: bigint,
  translations: ContentTranslationLike[],
) {
  const resolvedLocale = resolveLocale(locale);
  const tr = pickContentTranslation(translations, resolvedLocale);
  const title =
    tr?.title?.trim() ||
    BANNER_PROMO_DEFAULT_TITLE[resolvedLocale] ||
    BANNER_PROMO_DEFAULT_TITLE['en-US'];
  const rawBody = tr?.body?.trim() ? stripHtml(tr.body) : '';
  const body = rawBody || buildBannerPromoFallbackBody(resolvedLocale, title);
  const payload: BannerPromoPayload = { contentId: contentId.toString() };
  return {
    type: 'BANNER_PROMO' as const,
    title,
    body,
    payload,
  };
}

function buildAnnouncementPromoMessage(
  locale: string | null | undefined,
  contentId: bigint,
  translations: ContentTranslationLike[],
) {
  const resolvedLocale = resolveLocale(locale);
  const tr = pickContentTranslation(translations, resolvedLocale);
  const title =
    tr?.title?.trim() ||
    (tr?.body?.trim() ? tr.body.trim().slice(0, 40) : '') ||
    ANNOUNCEMENT_PROMO_DEFAULT_TITLE[resolvedLocale] ||
    ANNOUNCEMENT_PROMO_DEFAULT_TITLE['en-US'];
  const rawBody = tr?.body?.trim() ?? '';
  const body = rawBody || buildAnnouncementPromoFallbackBody(resolvedLocale, title);
  const payload: BannerPromoPayload = { contentId: contentId.toString() };
  return {
    type: 'ANNOUNCEMENT_PROMO' as const,
    title,
    body,
    payload,
  };
}

function mapMessageRow(row: {
  id: bigint;
  type: string;
  title: string;
  body: string;
  payload: Prisma.JsonValue;
  readAt: Date | null;
  createdAt: Date;
}) {
  return {
    id: row.id.toString(),
    type: row.type,
    title: row.title,
    body: row.body,
    payload: row.payload ?? null,
    readAt: row.readAt?.toISOString() ?? null,
    createdAt: row.createdAt.toISOString(),
    isRead: row.readAt != null,
  };
}

export type BroadcastTranslation = {
  title: string;
  body: string;
};

export type BroadcastTranslations = Record<string, BroadcastTranslation>;

export type BroadcastTranslationInput = {
  locale: string;
  title?: string | null;
  body?: string | null;
};

function normalizeBroadcastTranslations(
  inputs: BroadcastTranslationInput[],
): BroadcastTranslations {
  const map: BroadcastTranslations = {};
  for (const tr of inputs) {
    const locale = resolveLocale(tr.locale);
    map[locale] = {
      title: tr.title?.trim() ?? '',
      body: tr.body?.trim() ?? '',
    };
  }
  return map;
}

function resolveBroadcastContent(
  translations: BroadcastTranslations,
  locale: string | null | undefined,
): BroadcastTranslation | null {
  const chain = [resolveLocale(locale), 'en-US', 'zh-CN', 'ms-MY'];
  const seen = new Set<string>();
  for (const loc of chain) {
    if (seen.has(loc)) continue;
    seen.add(loc);
    const tr = translations[loc];
    if (!tr) continue;
    const title = tr.title?.trim();
    const body = tr.body?.trim();
    if (title || body) {
      return {
        title: title || stripHtml(body).slice(0, 256) || 'Notification',
        body: body || '',
      };
    }
  }
  for (const tr of Object.values(translations)) {
    const title = tr.title?.trim();
    const body = tr.body?.trim();
    if (title || body) {
      return {
        title: title || stripHtml(body).slice(0, 256) || 'Notification',
        body: body || '',
      };
    }
  }
  return null;
}

function parseBroadcastTranslations(value: Prisma.JsonValue | null | undefined): BroadcastTranslations {
  if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
  const map: BroadcastTranslations = {};
  for (const [locale, raw] of Object.entries(value as Record<string, unknown>)) {
    if (!raw || typeof raw !== 'object' || Array.isArray(raw)) continue;
    const row = raw as Record<string, unknown>;
    map[locale] = {
      title: typeof row.title === 'string' ? row.title : '',
      body: typeof row.body === 'string' ? row.body : '',
    };
  }
  return map;
}

function mapBroadcastRow(row: {
  id: bigint;
  title: string;
  body: string;
  translations: Prisma.JsonValue | null;
  targetType: string;
  targetUserId: bigint | null;
  targetUsername: string | null;
  recipientCount: number;
  createdById: bigint | null;
  createdByUsername: string | null;
  createdAt: Date;
}) {
  const translations = parseBroadcastTranslations(row.translations);
  const hasTranslations = Object.keys(translations).length > 0;
  const preview =
    resolveBroadcastContent(hasTranslations ? translations : { 'en-US': { title: row.title, body: row.body } }, 'en-US') ??
    { title: row.title, body: row.body };

  return {
    id: row.id.toString(),
    title: preview.title,
    body: preview.body,
    translations: hasTranslations
      ? translations
      : { 'en-US': { title: row.title, body: row.body } },
    targetType: row.targetType,
    targetUserId: row.targetUserId?.toString() ?? null,
    targetUsername: row.targetUsername,
    recipientCount: row.recipientCount,
    createdById: row.createdById?.toString() ?? null,
    createdByUsername: row.createdByUsername,
    createdAt: row.createdAt.toISOString(),
  };
}

@Injectable()
export class PlayerMessagesService {
  constructor(private prisma: PrismaService) {}

  private buildDepositMessage(
    type: Exclude<PlayerMessageType, 'BANNER_PROMO' | 'ANNOUNCEMENT_PROMO' | 'ADMIN_CUSTOM'>,
    locale: string | null | undefined,
    payload: DepositMessagePayload,
  ) {
    const resolvedLocale = resolveLocale(locale);
    const templates = MESSAGE_TEMPLATES[type];
    const template = templates[resolvedLocale] ?? templates['en-US'];
    return {
      type,
      title: template.title,
      body: template.body(payload),
      payload,
    };
  }

  async createDepositApprovedMessage(
    userId: bigint,
    data: {
      depositOrderId: bigint;
      orderNo: string;
      amount: string;
      approvedAmount: string;
      locale?: string | null;
    },
    client: Prisma.TransactionClient | PrismaService = this.prisma,
  ) {
    const payload: DepositMessagePayload = {
      depositOrderId: data.depositOrderId.toString(),
      orderNo: data.orderNo,
      amount: data.amount,
      approvedAmount: data.approvedAmount,
    };
    const content = this.buildDepositMessage('DEPOSIT_APPROVED', data.locale, payload);
    const row = await client.playerMessage.create({
      data: {
        userId,
        type: content.type,
        title: content.title,
        body: content.body,
        payload: content.payload as Prisma.InputJsonValue,
      },
    });
    return mapMessageRow(row);
  }

  async createDepositRejectedMessage(
    userId: bigint,
    data: {
      depositOrderId: bigint;
      orderNo: string;
      amount: string;
      rejectReason?: string | null;
      locale?: string | null;
    },
    client: Prisma.TransactionClient | PrismaService = this.prisma,
  ) {
    const payload: DepositMessagePayload = {
      depositOrderId: data.depositOrderId.toString(),
      orderNo: data.orderNo,
      amount: data.amount,
      rejectReason: data.rejectReason ?? null,
    };
    const content = this.buildDepositMessage('DEPOSIT_REJECTED', data.locale, payload);
    const row = await client.playerMessage.create({
      data: {
        userId,
        type: content.type,
        title: content.title,
        body: content.body,
        payload: content.payload as Prisma.InputJsonValue,
      },
    });
    return mapMessageRow(row);
  }

  async broadcastBannerPromotion(data: {
    contentId: bigint;
    translations: ContentTranslationLike[];
  }) {
    const players = await this.prisma.user.findMany({
      where: { userType: 'PLAYER', deletedAt: null, status: 'ACTIVE' },
      select: {
        id: true,
        locale: true,
        preferences: { select: { locale: true } },
      },
    });

    if (!players.length) return 0;

    const rows = players.map((player) => {
      const playerLocale = player.preferences?.locale ?? player.locale;
      const content = buildBannerPromoMessage(
        playerLocale,
        data.contentId,
        data.translations,
      );
      return {
        userId: player.id,
        type: content.type,
        title: content.title,
        body: content.body,
        payload: content.payload as Prisma.InputJsonValue,
      };
    });

    const batchSize = 200;
    for (let i = 0; i < rows.length; i += batchSize) {
      await this.prisma.playerMessage.createMany({ data: rows.slice(i, i + batchSize) });
    }

    return rows.length;
  }

  async broadcastAnnouncementPromotion(data: {
    contentId: bigint;
    translations: ContentTranslationLike[];
  }) {
    const players = await this.prisma.user.findMany({
      where: { userType: 'PLAYER', deletedAt: null, status: 'ACTIVE' },
      select: {
        id: true,
        locale: true,
        preferences: { select: { locale: true } },
      },
    });

    if (!players.length) return 0;

    const rows = players.map((player) => {
      const playerLocale = player.preferences?.locale ?? player.locale;
      const content = buildAnnouncementPromoMessage(
        playerLocale,
        data.contentId,
        data.translations,
      );
      return {
        userId: player.id,
        type: content.type,
        title: content.title,
        body: content.body,
        payload: content.payload as Prisma.InputJsonValue,
      };
    });

    const batchSize = 200;
    for (let i = 0; i < rows.length; i += batchSize) {
      await this.prisma.playerMessage.createMany({ data: rows.slice(i, i + batchSize) });
    }

    return rows.length;
  }

  async listForPlayer(userId: bigint, page = 1, pageSize = 20) {
    const safePage = Math.max(1, page);
    const safePageSize = Math.min(50, Math.max(1, pageSize));
    const skip = (safePage - 1) * safePageSize;
    const where = { userId };

    const [rows, total, unreadCount] = await Promise.all([
      this.prisma.playerMessage.findMany({
        where,
        orderBy: { createdAt: 'desc' },
        skip,
        take: safePageSize,
      }),
      this.prisma.playerMessage.count({ where }),
      this.prisma.playerMessage.count({ where: { ...where, readAt: null } }),
    ]);

    return {
      items: rows.map(mapMessageRow),
      total,
      unreadCount,
      page: safePage,
      pageSize: safePageSize,
    };
  }

  async getForPlayer(userId: bigint, messageId: bigint) {
    const row = await this.prisma.playerMessage.findFirst({
      where: { id: messageId, userId },
    });
    if (!row) throw appNotFound('MESSAGE_NOT_FOUND');
    return mapMessageRow(row);
  }

  async markRead(userId: bigint, messageId: bigint) {
    const row = await this.prisma.playerMessage.findFirst({
      where: { id: messageId, userId },
    });
    if (!row) throw appNotFound('MESSAGE_NOT_FOUND');
    if (row.readAt) return mapMessageRow(row);

    const updated = await this.prisma.playerMessage.update({
      where: { id: messageId },
      data: { readAt: new Date() },
    });
    return mapMessageRow(updated);
  }

  async markAllRead(userId: bigint) {
    const result = await this.prisma.playerMessage.updateMany({
      where: { userId, readAt: null },
      data: { readAt: new Date() },
    });
    return { updated: result.count };
  }

  async getUnreadCount(userId: bigint) {
    const unreadCount = await this.prisma.playerMessage.count({
      where: { userId, readAt: null },
    });
    return { unreadCount };
  }

  async deleteForPlayer(userId: bigint, messageId: bigint) {
    const row = await this.prisma.playerMessage.findFirst({
      where: { id: messageId, userId },
    });
    if (!row) throw appNotFound('MESSAGE_NOT_FOUND');
    await this.prisma.playerMessage.delete({ where: { id: messageId } });
    return { deleted: true, wasUnread: row.readAt == null };
  }

  async deleteAllForPlayer(userId: bigint) {
    const result = await this.prisma.playerMessage.deleteMany({ where: { userId } });
    return { deleted: result.count };
  }

  async listBroadcasts(page = 1, pageSize = 20) {
    const safePage = Math.max(1, page);
    const safePageSize = Math.min(50, Math.max(1, pageSize));
    const skip = (safePage - 1) * safePageSize;

    const [rows, total] = await Promise.all([
      this.prisma.playerMessageBroadcast.findMany({
        orderBy: { createdAt: 'desc' },
        skip,
        take: safePageSize,
      }),
      this.prisma.playerMessageBroadcast.count(),
    ]);

    return {
      items: rows.map(mapBroadcastRow),
      total,
      page: safePage,
      pageSize: safePageSize,
    };
  }

  async createCustomBroadcast(data: {
    translations: BroadcastTranslationInput[];
    targetType: 'ALL' | 'USER';
    targetUsername?: string | null;
    createdById?: bigint | null;
    createdByUsername?: string | null;
  }) {
    const translations = normalizeBroadcastTranslations(data.translations);
    const preview = resolveBroadcastContent(translations, 'en-US');
    if (!preview?.title && !preview?.body) {
      throw appBadRequest('BROADCAST_CONTENT_REQUIRED');
    }
    const title = preview.title.slice(0, 256);
    const body = preview.body;
    if (!title && !body) throw appBadRequest('BROADCAST_CONTENT_REQUIRED');

    let targetUsers: Array<{
      id: bigint;
      username: string;
      locale: string | null;
      preferences: { locale: string | null } | null;
    }> = [];
    let targetUserId: bigint | null = null;
    let targetUsername: string | null = null;

    if (data.targetType === 'ALL') {
      targetUsers = await this.prisma.user.findMany({
        where: { userType: 'PLAYER', deletedAt: null, status: 'ACTIVE' },
        select: {
          id: true,
          username: true,
          locale: true,
          preferences: { select: { locale: true } },
        },
      });
    } else {
      const username = data.targetUsername?.trim();
      if (!username) throw appBadRequest('BROADCAST_TARGET_USER_REQUIRED');
      const player = await this.prisma.user.findFirst({
        where: {
          username,
          userType: 'PLAYER',
          deletedAt: null,
          status: 'ACTIVE',
        },
        select: {
          id: true,
          username: true,
          locale: true,
          preferences: { select: { locale: true } },
        },
      });
      if (!player) throw appNotFound('PLAYER_NOT_FOUND');
      targetUsers = [player];
      targetUserId = player.id;
      targetUsername = player.username;
    }

    if (!targetUsers.length) throw appBadRequest('BROADCAST_NO_RECIPIENTS');

    const broadcast = await this.prisma.$transaction(async (tx) => {
      const created = await tx.playerMessageBroadcast.create({
        data: {
          title,
          body,
          translations: translations as Prisma.InputJsonValue,
          targetType: data.targetType,
          targetUserId,
          targetUsername,
          recipientCount: targetUsers.length,
          createdById: data.createdById ?? null,
          createdByUsername: data.createdByUsername ?? null,
        },
      });

      const rows = targetUsers.map((player) => {
        const playerLocale = player.preferences?.locale ?? player.locale;
        const content =
          resolveBroadcastContent(translations, playerLocale) ?? preview;
        return {
          userId: player.id,
          type: 'ADMIN_CUSTOM',
          title: content.title.slice(0, 256),
          body: content.body,
          broadcastId: created.id,
          payload: { broadcastId: created.id.toString() } as Prisma.InputJsonValue,
        };
      });

      const batchSize = 200;
      for (let i = 0; i < rows.length; i += batchSize) {
        await tx.playerMessage.createMany({ data: rows.slice(i, i + batchSize) });
      }

      return created;
    });

    return mapBroadcastRow(broadcast);
  }

  async deleteBroadcast(broadcastId: bigint) {
    const row = await this.prisma.playerMessageBroadcast.findUnique({
      where: { id: broadcastId },
    });
    if (!row) throw appNotFound('BROADCAST_NOT_FOUND');
    await this.prisma.playerMessageBroadcast.delete({ where: { id: broadcastId } });
    return { deleted: true, recipientCount: row.recipientCount };
  }
}
