import {
  BadRequestException,
  ForbiddenException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import {
  LuckyDrawStatus,
  ModerationStatus,
  Prisma,
  UserRole,
} from '@prisma/client';
import { PrismaService } from '../../database/prisma.service';
import { CreateLuckyDrawDto } from './dto/create-lucky-draw.dto';
import { UpdateLuckyDrawDto } from './dto/update-lucky-draw.dto';
import { UploadsService } from '../uploads/uploads.service';
import type { RejectLuckyDrawDto } from './dto/reject-lucky-draw.dto';
import { PlanLimitException } from '../plans/plan-limit.exception';
import { parseMonthlyLimit } from '../plans/plan-limits';

type RequestUser = {
  id: string;
  role: UserRole;
  shopId: string | null;
};

function parseDateOnly(s?: string | null) {
  if (s == null || s === '') throw new BadRequestException('Date is required');
  // Accept YYYY-MM-DD (from <input type="date">) or ISO strings
  const d = new Date(s);
  if (Number.isNaN(d.getTime())) throw new BadRequestException('Invalid date');
  return d;
}

function escapeXml(value: unknown) {
  return String(value ?? '')
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&apos;');
}

function designToSvg(design: any) {
  const d = design && typeof design === 'object' ? design : {};
  const width = 1200;
  const height = 600;
  const bgColor = String(d.bgColor || '#6366f1');
  const bgGradientEnd = String(d.bgGradientEnd || '#a855f7');
  const useGradient = !!d.useGradient;
  const angle = Number.isFinite(Number(d.bgGradientAngle))
    ? Number(d.bgGradientAngle)
    : 135;
  const borderRadius = Number.isFinite(Number(d.borderRadius))
    ? Number(d.borderRadius)
    : 32;
  const borderWidth =
    d.borderStyle && d.borderStyle !== 'none'
      ? Math.max(0, Number(d.borderWidth || 0))
      : 0;
  const borderColor = String(d.borderColor || '#ffffff40');
  const overlayOpacity = Math.max(
    0,
    Math.min(1, Number(d.overlayOpacity ?? 50) / 100),
  );
  const overlayColor = String(d.overlayColor || '#000000');
  const bgImage = String(d.bgImage || '').trim();
  const elements = Array.isArray(d.elements) ? d.elements : [];

  const radians = ((angle - 90) * Math.PI) / 180;
  const x1 = 50 + Math.cos(radians + Math.PI) * 50;
  const y1 = 50 + Math.sin(radians + Math.PI) * 50;
  const x2 = 50 + Math.cos(radians) * 50;
  const y2 = 50 + Math.sin(radians) * 50;

  const defs: string[] = [];
  if (useGradient) {
    defs.push(
      `<linearGradient id="bgGradient" x1="${x1}%" y1="${y1}%" x2="${x2}%" y2="${y2}%">` +
        `<stop offset="0%" stop-color="${escapeXml(bgColor)}" />` +
        `<stop offset="100%" stop-color="${escapeXml(bgGradientEnd)}" />` +
      `</linearGradient>`,
    );
  }

  const content: string[] = [];
  content.push(
    `<rect x="0" y="0" width="${width}" height="${height}" rx="${borderRadius}" ry="${borderRadius}" fill="${
      useGradient ? 'url(#bgGradient)' : escapeXml(bgColor)
    }" />`,
  );

  if (bgImage) {
    content.push(
      `<image href="${escapeXml(bgImage)}" x="0" y="0" width="${width}" height="${height}" preserveAspectRatio="xMidYMid slice" />`,
      `<rect x="0" y="0" width="${width}" height="${height}" rx="${borderRadius}" ry="${borderRadius}" fill="${escapeXml(
        overlayColor,
      )}" opacity="${overlayOpacity}" />`,
    );
  }

  if (borderWidth > 0) {
    const inset = borderWidth / 2;
    content.push(
      `<rect x="${inset}" y="${inset}" width="${width - borderWidth}" height="${
        height - borderWidth
      }" rx="${Math.max(0, borderRadius - inset)}" ry="${Math.max(
        0,
        borderRadius - inset,
      )}" fill="none" stroke="${escapeXml(borderColor)}" stroke-width="${borderWidth}" />`,
    );
  }

  for (const el of elements) {
    const x = (Number(el?.x ?? 50) / 100) * width;
    const y = (Number(el?.y ?? 50) / 100) * height;
    const rotation = Number(el?.rotation ?? 0);
    const opacity = Math.max(0, Math.min(1, Number(el?.opacity ?? 100) / 100));

    if (el?.type === 'text') {
      const fontSize = Math.max(10, Number(el?.fontSize ?? 16) * 3);
      const fontFamily = escapeXml(el?.fontFamily || 'Inter');
      const fill = escapeXml(el?.color || '#ffffff');
      const fontWeight = el?.bold ? '700' : '400';
      const fontStyle = el?.italic ? 'italic' : 'normal';
      const textDecoration = el?.underline ? 'underline' : 'none';
      const anchor =
        el?.align === 'left'
          ? 'start'
          : el?.align === 'right'
            ? 'end'
            : 'middle';
      content.push(
        `<text x="${x}" y="${y}" fill="${fill}" font-family="${fontFamily}" font-size="${fontSize}" font-weight="${fontWeight}" font-style="${fontStyle}" text-decoration="${textDecoration}" text-anchor="${anchor}" dominant-baseline="middle" opacity="${opacity}" transform="rotate(${rotation} ${x} ${y})">${escapeXml(
          el?.content || '',
        )}</text>`,
      );
      continue;
    }

    if (el?.type === 'image' && el?.imageSrc) {
      const imageWidth = Math.max(24, Number(el?.imageWidth ?? 120) * 3);
      const imageHeight = Math.max(24, Number(el?.imageHeight ?? 120) * 3);
      const imageX = x - imageWidth / 2;
      const imageY = y - imageHeight / 2;
      content.push(
        `<image href="${escapeXml(el.imageSrc)}" x="${imageX}" y="${imageY}" width="${imageWidth}" height="${imageHeight}" opacity="${opacity}" transform="rotate(${rotation} ${x} ${y})" preserveAspectRatio="xMidYMid meet" />`,
      );
    }
  }

  const defsBlock = defs.length ? `<defs>${defs.join('')}</defs>` : '';
  return (
    `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">` +
    defsBlock +
    content.join('') +
    `</svg>`
  );
}

@Injectable()
export class LuckyDrawService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly uploads: UploadsService,
  ) {}

  private async generateLuckyDrawImageFromDesign(
    shopId: string,
    luckyDrawId: string,
    design: unknown,
  ) {
    const svg = designToSvg(design);
    return this.uploads.saveImage({
      shopId,
      entityType: 'lucky-draw',
      entityId: luckyDrawId,
      purpose: 'active',
      ext: '.svg',
      buffer: Buffer.from(svg, 'utf8'),
    });
  }

  private async assertCanAccessShop(user: RequestUser, shopId: string) {
    if (user.role === UserRole.SUPERADMIN) return;
    if (user.role === UserRole.ADMIN) {
      const shop = await this.prisma.shop.findUnique({
        where: { id: shopId },
        select: { adminId: true },
      });
      if (!shop) throw new NotFoundException('Shop not found');
      if (shop.adminId !== user.id) throw new ForbiddenException('Forbidden');
      return;
    }
    // SUBADMIN scoped to current shop only
    if (!user.shopId || user.shopId !== shopId)
      throw new ForbiddenException('Forbidden');
  }

  private async listAllowedShopIdsForUser(
    user: RequestUser,
  ): Promise<string[]> {
    if (user.role === UserRole.SUPERADMIN) {
      const shops = await this.prisma.shop.findMany({ select: { id: true } });
      return shops.map((s) => s.id);
    }
    if (user.role === UserRole.ADMIN) {
      const shops = await this.prisma.shop.findMany({
        where: { adminId: user.id },
        select: { id: true },
      });
      return shops.map((s) => s.id);
    }
    return user.shopId ? [user.shopId] : [];
  }

  private async createForShop(
    user: RequestUser,
    shopId: string,
    dto: CreateLuckyDrawDto,
    opts?: { tmpMode?: 'move' | 'copy' },
  ) {
    await this.assertCanAccessShop(user, shopId);

    // Plan limits: Lucky Draw/month
    const shop = await this.prisma.shop.findUnique({
      where: { id: shopId },
      select: {
        admin: {
          select: {
            adminSubscription: {
              select: {
                plan: { select: { code: true, features: true } },
              },
            },
          },
        },
      },
    });
    if (!shop) throw new NotFoundException('Shop not found');
    const sharedPlan = shop.admin.adminSubscription?.plan;
    if (!sharedPlan) throw new ForbiddenException('Admin subscription is missing');
    const limit = parseMonthlyLimit(sharedPlan.features, 'Lucky Draw');
    if (limit !== 'unlimited') {
      if (limit <= 0) {
        throw new PlanLimitException({
          featureKey: 'Lucky Draw',
          currentPlanCode: sharedPlan.code,
          limit,
          used: 0,
          period: 'MONTH',
          message: 'Your plan does not allow Lucky Draw',
        });
      }
      const now = new Date();
      const start = new Date(now.getFullYear(), now.getMonth(), 1);
      const end = new Date(now.getFullYear(), now.getMonth() + 1, 1);
      const used = await this.prisma.luckyDraw.count({
        where: { shopId, createdAt: { gte: start, lt: end } },
      });
      if (used >= limit) {
        throw new PlanLimitException({
          featureKey: 'Lucky Draw',
          currentPlanCode: sharedPlan.code,
          limit,
          used,
          period: 'MONTH',
          message: `Lucky draw limit reached for your plan (limit: ${limit}/month)`,
        });
      }
    }

    const startDate = parseDateOnly(dto.startDate);
    const endDate = parseDateOnly(dto.endDate);
    if (startDate.getTime() > endDate.getTime()) {
      throw new BadRequestException('endDate must be on/after startDate');
    }

    const created = await this.prisma.luckyDraw.create({
      data: {
        shopId,
        name: dto.name,
        description: dto.description ?? null,
        prize: dto.prize ?? null,
        status: dto.status ?? undefined,
        moderationStatus: ModerationStatus.DRAFT,
        submittedAt: null,
        publishedAt: null,
        rejectedAt: null,
        rejectedReason: null,
        startDate,
        endDate,
        design: dto.design != null ? (dto.design as any) : null,
      },
    });

    if (created.design) {
      const rewritten = await this.uploads.rewriteDesignAssets(
        shopId,
        created.design as any,
        {
          bgRelDir: `shops/${shopId}/lucky-draws/${created.id}/background`,
          stickerRelDir: `shops/${shopId}/lucky-draws/${created.id}/sticker`,
        },
        { mode: opts?.tmpMode ?? 'move' },
      );
      if (rewritten !== created.design) {
        return this.prisma.luckyDraw.update({
          where: { id: created.id },
          data: { design: rewritten as any },
        });
      }
    }
    return created;
  }

  async list(user: RequestUser, shopIdQuery?: string) {
    if (user.role === UserRole.SUPERADMIN) {
      return this.prisma.luckyDraw.findMany({
        where: shopIdQuery ? { shopId: shopIdQuery } : undefined,
        orderBy: { createdAt: 'desc' },
      });
    }
    if (!user.shopId) throw new ForbiddenException('No shop selected');
    await this.assertCanAccessShop(user, user.shopId);
    return this.prisma.luckyDraw.findMany({
      where: { shopId: user.shopId },
      orderBy: { createdAt: 'desc' },
    });
  }

  async listPaged(
    user: RequestUser,
    args: {
      page: number;
      limit: number;
      q?: string;
      status?: string;
      moderationStatus?: string;
      shopId?: string;
    },
  ) {
    const page = Math.max(1, Math.floor(args.page || 1));
    const limit = Math.min(100, Math.max(1, Math.floor(args.limit || 20)));
    const skip = (page - 1) * limit;
    const q = String(args.q ?? '').trim();
    const statusRaw =
      args.status != null ? String(args.status).trim().toUpperCase() : '';
    const modRaw =
      args.moderationStatus != null
        ? String(args.moderationStatus).trim().toUpperCase()
        : '';

    const shopId =
      user.role === UserRole.SUPERADMIN
        ? args.shopId?.trim() || user.shopId || null
        : (user.shopId ?? null);
    if (!shopId) throw new ForbiddenException('No shop selected');
    await this.assertCanAccessShop(user, shopId);

    let status: LuckyDrawStatus | undefined;
    if (statusRaw === 'DRAFT') status = LuckyDrawStatus.DRAFT;
    else if (statusRaw === 'ACTIVE') status = LuckyDrawStatus.ACTIVE;
    else if (statusRaw === 'UPCOMING') status = LuckyDrawStatus.UPCOMING;
    else if (statusRaw === 'ENDED') status = LuckyDrawStatus.ENDED;

    let moderationStatus: ModerationStatus | undefined;
    if (modRaw === 'DRAFT') moderationStatus = ModerationStatus.DRAFT;
    else if (modRaw === 'SUBMITTED')
      moderationStatus = ModerationStatus.SUBMITTED;
    else if (modRaw === 'PUBLISHED')
      moderationStatus = ModerationStatus.PUBLISHED;
    else if (modRaw === 'REJECTED')
      moderationStatus = ModerationStatus.REJECTED;

    const where: Prisma.LuckyDrawWhereInput = { shopId };
    if (status) where.status = status;
    if (moderationStatus) where.moderationStatus = moderationStatus;
    if (q) {
      where.OR = [
        { id: { contains: q, mode: 'insensitive' } },
        { name: { contains: q, mode: 'insensitive' } },
        { description: { contains: q, mode: 'insensitive' } },
        { prize: { contains: q, mode: 'insensitive' } },
      ];
    }

    const [total, items] = await this.prisma.$transaction([
      this.prisma.luckyDraw.count({ where }),
      this.prisma.luckyDraw.findMany({
        where,
        orderBy: { createdAt: 'desc' },
        skip,
        take: limit,
      }),
    ]);

    return { page, limit, total, items };
  }

  async listAdminPaged(args: {
    page: number;
    limit: number;
    q?: string;
    shopId?: string;
    status?: string;
    moderationStatus?: string;
  }) {
    const page = Math.max(1, Math.floor(args.page || 1));
    const limit = Math.min(100, Math.max(1, Math.floor(args.limit || 20)));
    const skip = (page - 1) * limit;
    const q = String(args.q ?? '').trim();
    const shopId = String(args.shopId ?? '').trim();
    const statusRaw =
      args.status != null ? String(args.status).trim().toUpperCase() : '';
    const modRaw =
      args.moderationStatus != null
        ? String(args.moderationStatus).trim().toUpperCase()
        : '';

    let status: LuckyDrawStatus | undefined;
    if (statusRaw === 'DRAFT') status = LuckyDrawStatus.DRAFT;
    else if (statusRaw === 'ACTIVE') status = LuckyDrawStatus.ACTIVE;
    else if (statusRaw === 'UPCOMING') status = LuckyDrawStatus.UPCOMING;
    else if (statusRaw === 'ENDED') status = LuckyDrawStatus.ENDED;

    let moderationStatus: ModerationStatus | undefined;
    if (modRaw === 'DRAFT') moderationStatus = ModerationStatus.DRAFT;
    else if (modRaw === 'SUBMITTED')
      moderationStatus = ModerationStatus.SUBMITTED;
    else if (modRaw === 'PUBLISHED')
      moderationStatus = ModerationStatus.PUBLISHED;
    else if (modRaw === 'REJECTED')
      moderationStatus = ModerationStatus.REJECTED;

    const where: Prisma.LuckyDrawWhereInput = {};
    if (shopId) where.shopId = shopId;
    if (status) where.status = status;
    if (moderationStatus) where.moderationStatus = moderationStatus;
    if (q) {
      where.OR = [
        { id: { contains: q, mode: 'insensitive' } },
        { name: { contains: q, mode: 'insensitive' } },
        { description: { contains: q, mode: 'insensitive' } },
        { prize: { contains: q, mode: 'insensitive' } },
        { shop: { name: { contains: q, mode: 'insensitive' } } },
      ];
    }

    const [total, items] = await this.prisma.$transaction([
      this.prisma.luckyDraw.count({ where }),
      this.prisma.luckyDraw.findMany({
        where,
        orderBy: { createdAt: 'desc' },
        skip,
        take: limit,
        select: {
          id: true,
          shopId: true,
          name: true,
          description: true,
          prize: true,
          status: true,
          moderationStatus: true,
          publishedAt: true,
          rejectedAt: true,
          rejectedReason: true,
          startDate: true,
          endDate: true,
          imageUrl: true,
          participantsCount: true,
          createdAt: true,
          updatedAt: true,
          shop: {
            select: {
              id: true,
              name: true,
              username: true,
              city: true,
              state: true,
            },
          },
        },
      }),
    ]);

    return { page, limit, total, items };
  }

  async publish(id: string) {
    const existing = await this.prisma.luckyDraw.findUnique({
      where: { id },
      select: {
        id: true,
        shopId: true,
        startDate: true,
        endDate: true,
        status: true,
        imageUrl: true,
        design: true,
      },
    });
    if (!existing) throw new NotFoundException('Lucky draw not found');

    const now = new Date();
    const inferredStatus =
      existing.endDate.getTime() < now.getTime()
        ? LuckyDrawStatus.ENDED
        : existing.startDate.getTime() > now.getTime()
          ? LuckyDrawStatus.UPCOMING
          : LuckyDrawStatus.ACTIVE;

    const generatedImageUrl =
      !existing.imageUrl && existing.design
        ? await this.generateLuckyDrawImageFromDesign(
            existing.shopId,
            existing.id,
            existing.design,
          )
        : null;
    const finalImageUrl = generatedImageUrl || existing.imageUrl;
    if (!finalImageUrl || String(finalImageUrl).trim() === '') {
      throw new BadRequestException(
        'Lucky draw image is missing and no design is available to generate it.',
      );
    }

    const updated = await this.prisma.luckyDraw.update({
      where: { id },
      data: {
        imageUrl: finalImageUrl,
        moderationStatus: ModerationStatus.PUBLISHED,
        publishedAt: now,
        rejectedAt: null,
        rejectedReason: null,
        status:
          existing.status === LuckyDrawStatus.DRAFT
            ? inferredStatus
            : existing.status,
      },
      select: {
        id: true,
        status: true,
        moderationStatus: true,
        publishedAt: true,
      },
    });

    return { ok: true, luckyDraw: updated };
  }

  async reject(id: string, dto: RejectLuckyDrawDto) {
    const existing = await this.prisma.luckyDraw.findUnique({
      where: { id },
      select: { id: true },
    });
    if (!existing) throw new NotFoundException('Lucky draw not found');
    const reason = String(dto?.reason ?? '').trim();
    if (!reason) throw new BadRequestException('reason is required');

    const now = new Date();
    const updated = await this.prisma.luckyDraw.update({
      where: { id },
      data: {
        moderationStatus: ModerationStatus.REJECTED,
        submittedAt: null,
        rejectedAt: now,
        rejectedReason: reason,
        publishedAt: null,
        status: LuckyDrawStatus.DRAFT,
      },
      select: {
        id: true,
        status: true,
        moderationStatus: true,
        rejectedAt: true,
        rejectedReason: true,
      },
    });

    return { ok: true, luckyDraw: updated };
  }

  async submit(user: RequestUser, id: string) {
    const existing = await this.prisma.luckyDraw.findUnique({
      where: { id },
      select: {
        id: true,
        shopId: true,
        moderationStatus: true,
        imageUrl: true,
        design: true,
      },
    });
    if (!existing) throw new NotFoundException('Lucky draw not found');
    await this.assertCanAccessShop(user, existing.shopId);
    if (existing.moderationStatus === ModerationStatus.PUBLISHED) {
      throw new BadRequestException('Lucky draw is already published');
    }

    const generatedImageUrl = existing.design
      ? await this.generateLuckyDrawImageFromDesign(
          existing.shopId,
          existing.id,
          existing.design,
        )
      : null;
    const finalImageUrl = generatedImageUrl || existing.imageUrl;
    if (!finalImageUrl || String(finalImageUrl).trim() === '') {
      throw new BadRequestException(
        'Lucky draw image is missing and no design is available to generate it.',
      );
    }

    const now = new Date();
    const updated = await this.prisma.luckyDraw.update({
      where: { id },
      data: {
        imageUrl: finalImageUrl,
        moderationStatus: ModerationStatus.SUBMITTED,
        submittedAt: now,
        rejectedAt: null,
        rejectedReason: null,
      },
      select: { id: true, moderationStatus: true, submittedAt: true },
    });
    return { ok: true, luckyDraw: updated };
  }

  async create(user: RequestUser, dto: CreateLuckyDrawDto) {
    if (!user.shopId) throw new ForbiddenException('No shop selected');
    return this.createForShop(user, user.shopId, dto, { tmpMode: 'move' });
  }

  async bulkCreate(
    user: RequestUser,
    dto: CreateLuckyDrawDto & { shopIds?: string[]; allShops?: boolean },
  ) {
    if (user.role === UserRole.SUBADMIN) {
      throw new ForbiddenException('Not allowed');
    }
    const allowed = await this.listAllowedShopIdsForUser(user);
    if (allowed.length === 0)
      throw new ForbiddenException('No shops available');

    let targets: string[] = [];
    if (dto.allShops) {
      targets = allowed;
    } else if (dto.shopIds && dto.shopIds.length > 0) {
      const set = new Set(allowed);
      targets = dto.shopIds.filter((id) => set.has(id));
    } else {
      if (!user.shopId) throw new ForbiddenException('No shop selected');
      targets = [user.shopId];
    }
    if (targets.length === 0)
      throw new ForbiddenException('No allowed shops selected');

    const created: any[] = [];
    for (const shopId of targets) {
      created.push(
        await this.createForShop(user, shopId, dto, { tmpMode: 'copy' }),
      );
    }
    return created;
  }

  async update(user: RequestUser, id: string, dto: UpdateLuckyDrawDto) {
    const existing = await this.prisma.luckyDraw.findUnique({
      where: { id },
      select: { id: true, shopId: true, startDate: true, endDate: true },
    });
    if (!existing) throw new NotFoundException('Lucky draw not found');
    await this.assertCanAccessShop(user, existing.shopId);

    const nextStart =
      dto.startDate === undefined
        ? existing.startDate
        : parseDateOnly(dto.startDate);
    const nextEnd =
      dto.endDate === undefined ? existing.endDate : parseDateOnly(dto.endDate);
    if (nextStart.getTime() > nextEnd.getTime()) {
      throw new BadRequestException('endDate must be on/after startDate');
    }

    const nextDesign =
      dto.design !== undefined
        ? await this.uploads.rewriteDesignAssets(
            existing.shopId,
            dto.design as any,
            {
              bgRelDir: `shops/${existing.shopId}/lucky-draws/${existing.id}/background`,
              stickerRelDir: `shops/${existing.shopId}/lucky-draws/${existing.id}/sticker`,
            },
            { mode: 'move' },
          )
        : undefined;

    return this.prisma.luckyDraw.update({
      where: { id },
      data: {
        ...(dto.name !== undefined ? { name: dto.name } : {}),
        ...(dto.description !== undefined
          ? { description: dto.description }
          : {}),
        ...(dto.prize !== undefined ? { prize: dto.prize } : {}),
        ...(dto.status !== undefined ? { status: dto.status } : {}),
        ...(dto.startDate !== undefined ? { startDate: nextStart } : {}),
        ...(dto.endDate !== undefined ? { endDate: nextEnd } : {}),
        ...(dto.design !== undefined ? { design: nextDesign as any } : {}),
      },
    });
  }

  async remove(user: RequestUser, id: string) {
    const existing = await this.prisma.luckyDraw.findUnique({
      where: { id },
      select: { id: true, shopId: true },
    });
    if (!existing) throw new NotFoundException('Lucky draw not found');
    await this.assertCanAccessShop(user, existing.shopId);
    await this.prisma.luckyDraw.delete({ where: { id } });
    return { ok: true };
  }
}
