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

export const CONTENT_TYPES = ['BANNER', 'NOTICE', 'TICKER'] as const;
export type ContentType = (typeof CONTENT_TYPES)[number];

/** 管理端合并 Tab：公告与滚动条在玩家端均为顶部跑马灯 */
export const ANNOUNCEMENT_ADMIN_TYPE = 'ANNOUNCEMENT';
export const ANNOUNCEMENT_TYPES: ContentType[] = ['NOTICE', 'TICKER'];

export const CONTENT_STATUSES = ['DRAFT', 'ACTIVE', 'INACTIVE'] as const;
export type ContentStatus = (typeof CONTENT_STATUSES)[number];

export const CONTENT_LINK_TYPES = ['ROUTE', 'URL'] as const;
export type ContentLinkType = (typeof CONTENT_LINK_TYPES)[number];

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

export type ContentTranslationInput = {
  locale: string;
  title?: string;
  body?: string;
  imageUrl?: string;
};

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 normalizeOptionalUrl(value?: string | null) {
  const v = value?.trim();
  return v || null;
}

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

  private assertContentType(type: string): ContentType {
    if (!CONTENT_TYPES.includes(type as ContentType)) {
      throw appBadRequest('CONTENT_TYPE_INVALID', { type });
    }
    return type as ContentType;
  }

  private assertStatus(status: string): ContentStatus {
    if (!CONTENT_STATUSES.includes(status as ContentStatus)) {
      throw appBadRequest('CONTENT_STATUS_INVALID', { status });
    }
    return status as ContentStatus;
  }

  private validateSchedule(startTime?: Date | null, endTime?: Date | null) {
    if (startTime && endTime && endTime <= startTime) {
      throw appBadRequest('CONTENT_END_BEFORE_START');
    }
  }

  private validateTranslations(
    contentType: ContentType,
    translations: ContentTranslationInput[],
    status: ContentStatus,
  ) {
    if (!translations.length) {
      throw appBadRequest('CONTENT_TRANSLATION_REQUIRED');
    }

    const locales = new Set<string>();
    for (const tr of translations) {
      if (!tr.locale?.trim()) {
        throw appBadRequest('CONTENT_LOCALE_REQUIRED');
      }
      if (locales.has(tr.locale)) {
        throw appBadRequest('CONTENT_LOCALE_DUPLICATE', { locale: tr.locale });
      }
      locales.add(tr.locale);
    }

    if (status !== 'ACTIVE') return;

    const hasUsable = translations.some((tr) => {
      if (contentType === 'BANNER') {
        return !!normalizeOptionalUrl(tr.imageUrl);
      }
      if (contentType === 'NOTICE') {
        return !!(tr.title?.trim() || tr.body?.trim());
      }
      return !!tr.body?.trim();
    });

    if (!hasUsable) {
      throw appBadRequest(
        contentType === 'BANNER'
          ? 'CONTENT_ACTIVE_BANNER_INCOMPLETE'
          : contentType === 'NOTICE'
            ? 'CONTENT_ACTIVE_NOTICE_INCOMPLETE'
            : 'CONTENT_ACTIVE_TICKER_INCOMPLETE',
      );
    }
  }

  private validateLink(linkType?: string | null, linkTarget?: string | null) {
    if (!linkType) return;
    if (!CONTENT_LINK_TYPES.includes(linkType as ContentLinkType)) {
      throw appBadRequest('CONTENT_LINK_TYPE_INVALID', { linkType });
    }
    if (!linkTarget?.trim()) {
      throw appBadRequest('CONTENT_LINK_TARGET_REQUIRED');
    }
  }

  playerVisibility(
    item: {
      status: string;
      startTime: Date | null;
      endTime: Date | null;
      contentType: string;
      translations: Array<{
        locale: string;
        title?: string | null;
        body?: string | null;
        imageUrl?: string | null;
      }>;
    },
    now = new Date(),
  ): { playerVisible: boolean; playerHiddenReason: string | null } {
    if (item.status !== 'ACTIVE') {
      return { playerVisible: false, playerHiddenReason: 'NOT_ACTIVE' };
    }
    if (item.startTime && item.startTime > now) {
      return { playerVisible: false, playerHiddenReason: 'NOT_STARTED' };
    }
    if (item.endTime && item.endTime < now) {
      return { playerVisible: false, playerHiddenReason: 'EXPIRED' };
    }

    const type = item.contentType as ContentType;
    const ok = item.translations.some((tr) => {
      if (type === 'BANNER') return !!normalizeOptionalUrl(tr.imageUrl);
      if (type === 'NOTICE') return !!(tr.title?.trim() || tr.body?.trim());
      return !!tr.body?.trim();
    });

    if (!ok) {
      return { playerVisible: false, playerHiddenReason: 'INCOMPLETE' };
    }

    return { playerVisible: true, playerHiddenReason: null };
  }

  private mapAdminItem(
    item: Awaited<ReturnType<typeof this.getRawById>>,
    now = new Date(),
  ) {
    const visibility = this.playerVisibility(item, now);
    const preview =
      pickContentTranslation(item.translations, 'zh-CN') ??
      pickContentTranslation(item.translations, 'en-US');
    return {
      id: item.id.toString(),
      contentType: item.contentType,
      sortOrder: item.sortOrder,
      status: item.status,
      linkType: item.linkType,
      linkTarget: item.linkTarget,
      startTime: item.startTime?.toISOString() ?? null,
      endTime: item.endTime?.toISOString() ?? null,
      createdAt: item.createdAt.toISOString(),
      updatedAt: item.updatedAt.toISOString(),
      previewTitle: preview?.title ?? preview?.body?.slice(0, 40) ?? '',
      previewImageUrl: preview?.imageUrl ?? null,
      playerVisible: visibility.playerVisible,
      playerHiddenReason: visibility.playerHiddenReason,
      translations: item.translations.map((tr) => ({
        locale: tr.locale,
        title: tr.title ?? '',
        body: tr.body ?? '',
        imageUrl: tr.imageUrl ?? '',
      })),
    };
  }

  async getRawById(id: bigint) {
    const item = await this.prisma.content.findUnique({
      where: { id },
      include: { translations: true },
    });
    if (!item) throw appNotFound('CONTENT_NOT_FOUND');
    return item;
  }

  async listActiveAnnouncements(locale: string) {
    const now = new Date();
    const items = await this.prisma.content.findMany({
      where: {
        contentType: { in: [...ANNOUNCEMENT_TYPES] },
        status: 'ACTIVE',
        OR: [{ startTime: null }, { startTime: { lte: now } }],
        AND: [{ OR: [{ endTime: null }, { endTime: { gte: now } }] }],
      },
      include: { translations: true },
      orderBy: { sortOrder: 'asc' },
    });

    return items
      .filter((item) => this.playerVisibility(item, now).playerVisible)
      .map((item) => {
        const tr = pickContentTranslation(item.translations, locale);
        return {
          id: item.id.toString(),
          contentType: item.contentType,
          sortOrder: item.sortOrder,
          createdAt: item.createdAt.toISOString(),
          linkType: item.linkType,
          linkTarget: item.linkTarget,
          translation: tr,
        };
      });
  }

  async listActive(contentType: string, locale: string) {
    const now = new Date();
    const type = this.assertContentType(contentType);
    const items = await this.prisma.content.findMany({
      where: {
        contentType,
        status: 'ACTIVE',
        OR: [{ startTime: null }, { startTime: { lte: now } }],
        AND: [{ OR: [{ endTime: null }, { endTime: { gte: now } }] }],
      },
      include: { translations: true },
      orderBy:
        type === 'BANNER'
          ? [{ createdAt: 'desc' }, { id: 'desc' }]
          : [{ sortOrder: 'asc' }, { id: 'asc' }],
    });

    return items
      .filter((item) => this.playerVisibility(item, now).playerVisible)
      .map((item) => {
        const t = pickContentTranslation(item.translations, locale);
        return {
          id: item.id.toString(),
          contentType: item.contentType,
          sortOrder: item.sortOrder,
          createdAt: item.createdAt.toISOString(),
          linkType: item.linkType,
          linkTarget: item.linkTarget,
          translation: t,
        };
      });
  }

  async listForAdmin(
    contentType?: string,
    status?: string,
    page = 1,
    pageSize = 20,
  ) {
    const typeWhere =
      contentType === ANNOUNCEMENT_ADMIN_TYPE
        ? { contentType: { in: [...ANNOUNCEMENT_TYPES] } }
        : contentType
          ? { contentType }
          : {};

    const where = {
      ...typeWhere,
      ...(status ? { status } : {}),
    };

    const [items, total] = await Promise.all([
      this.prisma.content.findMany({
        where,
        include: { translations: true },
        orderBy: [{ createdAt: 'desc' }, { id: 'desc' }],
        skip: (page - 1) * pageSize,
        take: pageSize,
      }),
      this.prisma.content.count({ where }),
    ]);

    return {
      items: items.map((item) => this.mapAdminItem(item)),
      total,
      page,
      pageSize,
    };
  }

  async getForAdmin(id: bigint) {
    return this.mapAdminItem(await this.getRawById(id));
  }

  async create(data: {
    contentType: string;
    sortOrder?: number;
    status?: string;
    linkType?: string | null;
    linkTarget?: string | null;
    startTime?: string | null;
    endTime?: string | null;
    translations: ContentTranslationInput[];
  }) {
    const contentType = this.assertContentType(data.contentType);
    const status = this.assertStatus(data.status ?? 'DRAFT');
    this.validateLink(data.linkType, data.linkTarget);
    this.validateTranslations(contentType, data.translations, status);

    const startTime = data.startTime ? new Date(data.startTime) : null;
    const endTime = data.endTime ? new Date(data.endTime) : null;
    this.validateSchedule(startTime, endTime);

    const item = await this.prisma.content.create({
      data: {
        contentType,
        sortOrder: data.sortOrder ?? 0,
        status,
        linkType: data.linkType?.trim() || null,
        linkTarget: data.linkTarget?.trim() || null,
        startTime,
        endTime,
        translations: {
          create: data.translations.map((tr) => ({
            locale: tr.locale.trim(),
            title: tr.title?.trim() || null,
            body: tr.body?.trim() || null,
            imageUrl: normalizeOptionalUrl(tr.imageUrl),
          })),
        },
      },
      include: { translations: true },
    });

    return this.mapAdminItem(item);
  }

  async update(
    id: bigint,
    data: {
      sortOrder?: number;
      status?: string;
      linkType?: string | null;
      linkTarget?: string | null;
      startTime?: string | null;
      endTime?: string | null;
      translations?: ContentTranslationInput[];
    },
  ) {
    const existing = await this.getRawById(id);
    const contentType = existing.contentType as ContentType;
    const status = data.status
      ? this.assertStatus(data.status)
      : (existing.status as ContentStatus);

    const linkType =
      data.linkType !== undefined
        ? data.linkType?.trim() || null
        : existing.linkType;
    const linkTarget =
      data.linkTarget !== undefined
        ? data.linkTarget?.trim() || null
        : existing.linkTarget;
    this.validateLink(linkType, linkTarget);

    const startTime =
      data.startTime !== undefined
        ? data.startTime
          ? new Date(data.startTime)
          : null
        : existing.startTime;
    const endTime =
      data.endTime !== undefined
        ? data.endTime
          ? new Date(data.endTime)
          : null
        : existing.endTime;
    this.validateSchedule(startTime, endTime);

    const translations =
      data.translations ??
      existing.translations.map((tr) => ({
        locale: tr.locale,
        title: tr.title ?? undefined,
        body: tr.body ?? undefined,
        imageUrl: tr.imageUrl ?? undefined,
      }));
    this.validateTranslations(contentType, translations, status);

    await this.prisma.content.update({
      where: { id },
      data: {
        sortOrder: data.sortOrder ?? existing.sortOrder,
        status,
        linkType,
        linkTarget,
        startTime,
        endTime,
      },
    });

    if (data.translations) {
      for (const tr of data.translations) {
        await this.prisma.contentTranslation.upsert({
          where: {
            contentId_locale: {
              contentId: id,
              locale: tr.locale.trim(),
            },
          },
          create: {
            contentId: id,
            locale: tr.locale.trim(),
            title: tr.title?.trim() || null,
            body: tr.body?.trim() || null,
            imageUrl: normalizeOptionalUrl(tr.imageUrl),
          },
          update: {
            title: tr.title?.trim() || null,
            body: tr.body?.trim() || null,
            imageUrl: normalizeOptionalUrl(tr.imageUrl),
          },
        });
      }
    }

    return this.getForAdmin(id);
  }

  async updateStatus(id: bigint, status: string) {
    const existing = await this.getRawById(id);
    const next = this.assertStatus(status);
    this.validateTranslations(
      existing.contentType as ContentType,
      existing.translations.map((tr) => ({
        locale: tr.locale,
        title: tr.title ?? undefined,
        body: tr.body ?? undefined,
        imageUrl: tr.imageUrl ?? undefined,
      })),
      next,
    );
    await this.prisma.content.update({
      where: { id },
      data: { status: next },
    });
    return this.getForAdmin(id);
  }

  async remove(id: bigint) {
    await this.getRawById(id);
    await this.prisma.contentTranslation.deleteMany({ where: { contentId: id } });
    await this.prisma.content.delete({ where: { id } });
    return { ok: true };
  }

  /** @deprecated use listForAdmin */
  async listAll(contentType?: string) {
    const result = await this.listForAdmin(contentType);
    return result.items;
  }
}
