import {
  BadRequestException,
  ForbiddenException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { PrismaService } from '../../database/prisma.service';
import { copyFile, mkdir, rename, stat, writeFile } from 'fs/promises';
import { join, normalize, sep } from 'path';
import { randomUUID } from 'crypto';

export type UploadEntityType =
  | 'tmp'
  | 'shop'
  | 'coupon'
  | 'lucky-draw'
  | 'template'
  | 'business-type';
export type UploadPurpose = 'active' | 'background' | 'sticker' | 'assets';

type UploadOpts = {
  shopId?: string;
  entityType: UploadEntityType;
  entityId?: string;
  purpose: UploadPurpose;
  ext: string;
  buffer: Buffer;
};

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

  private baseDir() {
    return join(process.cwd(), 'uploads');
  }

  private toRelDir(
    shopId: string | undefined,
    entityType: UploadEntityType,
    entityId: string | undefined,
    purpose: UploadPurpose,
  ) {
    if (entityType === 'business-type') {
      const raw = (entityId || 'general').toString().trim() || 'general';
      const safeId =
        raw
          .toLowerCase()
          .replace(/[^a-z0-9_-]+/g, '-')
          .replace(/^-+|-+$/g, '') || 'general';
      return `business-types/${safeId}/${purpose}`;
    }

    const safeShop = shopId;
    if (!safeShop) throw new BadRequestException('shopId is required');
    if (entityType === 'tmp') return `shops/${safeShop}/tmp/${purpose}`;
    if (entityType === 'shop') return `shops/${safeShop}/profile/${purpose}`;
    if (!entityId) throw new BadRequestException('entityId is required');

    if (entityType === 'coupon')
      return `shops/${safeShop}/coupons/${entityId}/${purpose}`;
    if (entityType === 'lucky-draw')
      return `shops/${safeShop}/lucky-draws/${entityId}/${purpose}`;
    if (entityType === 'template')
      return `shops/${safeShop}/templates/${entityId}/${purpose}`;
    throw new BadRequestException('Invalid entityType');
  }

  private publicPath(relFile: string) {
    // Always forward slashes in URLs.
    return `/uploads/${relFile.split(sep).join('/')}`;
  }

  private fsPathFromPublic(publicPath: string) {
    if (!publicPath.startsWith('/uploads/'))
      throw new BadRequestException('Invalid upload path');
    const rel = publicPath.slice('/uploads/'.length);
    const full = normalize(join(this.baseDir(), rel));
    const base = normalize(this.baseDir() + sep);
    if (!full.startsWith(base))
      throw new BadRequestException('Invalid upload path');
    return { rel, full };
  }

  private async assertEntityBelongsToShop(
    shopId: string,
    entityType: Exclude<UploadEntityType, 'tmp' | 'shop'>,
    entityId: string,
  ) {
    if (entityType === 'coupon') {
      const c = await this.prisma.coupon.findUnique({
        where: { id: entityId },
        select: { shopId: true },
      });
      if (!c) throw new NotFoundException('Coupon not found');
      if (c.shopId !== shopId) throw new ForbiddenException('Forbidden');
      return;
    }
    if (entityType === 'lucky-draw') {
      const d = await this.prisma.luckyDraw.findUnique({
        where: { id: entityId },
        select: { shopId: true },
      });
      if (!d) throw new NotFoundException('Lucky draw not found');
      if (d.shopId !== shopId) throw new ForbiddenException('Forbidden');
      return;
    }
    if (entityType === 'template') {
      const t = await this.prisma.designTemplate.findUnique({
        where: { id: entityId },
        select: { shopId: true },
      });
      if (!t) throw new NotFoundException('Template not found');
      if (t.shopId !== shopId) throw new ForbiddenException('Forbidden');
      return;
    }
  }

  async saveImage(opts: UploadOpts): Promise<string> {
    const relDir = this.toRelDir(
      opts.shopId,
      opts.entityType,
      opts.entityId,
      opts.purpose,
    );

    if (
      opts.entityType !== 'tmp' &&
      opts.entityType !== 'shop' &&
      opts.entityType !== 'business-type'
    ) {
      if (!opts.shopId) throw new BadRequestException('shopId is required');
      await this.assertEntityBelongsToShop(
        opts.shopId,
        opts.entityType,
        opts.entityId!,
      );
    }

    const filename = `${randomUUID()}${opts.ext}`;
    const relFile = `${relDir}/${filename}`;
    const fullDir = join(this.baseDir(), relDir);
    await mkdir(fullDir, { recursive: true });
    await writeFile(join(this.baseDir(), relFile), opts.buffer);
    return this.publicPath(relFile);
  }

  private tryParseTmpShopPath(
    publicPath: string,
  ): { rel: string; full: string; shopId: string } | null {
    if (!publicPath.startsWith('/uploads/shops/')) return null;
    const { rel, full } = this.fsPathFromPublic(publicPath);
    const parts = rel.split('/');
    // rel: shops/<shopId>/tmp/<purpose>/<filename>
    if (parts.length < 5) return null;
    if (parts[0] !== 'shops') return null;
    const shopId = parts[1];
    if (!shopId) return null;
    if (parts[2] !== 'tmp') return null;
    return { rel, full, shopId };
  }

  private extractShopIdFromDestRelDir(destRelDir: string): string | null {
    // destRelDir: shops/<shopId>/...
    const parts = destRelDir.split('/');
    if (parts.length < 2) return null;
    if (parts[0] !== 'shops') return null;
    return parts[1] || null;
  }

  private async relocateTmp(
    publicPath: string,
    destRelDir: string,
    mode: 'move' | 'copy',
  ): Promise<string> {
    const parsed = this.tryParseTmpShopPath(publicPath);
    if (!parsed) return publicPath;
    const { rel, full, shopId: srcShopId } = parsed;

    await stat(full).catch(() => {
      throw new NotFoundException('Uploaded file not found');
    });

    const parts = rel.split('/');
    const filename = parts[parts.length - 1];
    const destRelFile = `${destRelDir}/${filename}`;
    const destFullDir = join(this.baseDir(), destRelDir);
    await mkdir(destFullDir, { recursive: true });

    if (mode === 'copy') {
      await copyFile(full, join(this.baseDir(), destRelFile));
      return this.publicPath(destRelFile);
    }

    // move mode: move within same shop, copy across shops
    const destShopId = this.extractShopIdFromDestRelDir(destRelDir);
    if (destShopId && destShopId !== srcShopId) {
      await copyFile(full, join(this.baseDir(), destRelFile));
    } else {
      await rename(full, join(this.baseDir(), destRelFile));
    }
    return this.publicPath(destRelFile);
  }

  /**
   * Moves tmp asset if destination shop matches, otherwise copies it.
   * This enables "bulk create" across shops while uploads initially happen
   * under the user's current shop tmp directory.
   */
  async moveIfTmp(publicPath: string, destRelDir: string): Promise<string> {
    return this.relocateTmp(publicPath, destRelDir, 'move');
  }

  async copyIfTmp(publicPath: string, destRelDir: string): Promise<string> {
    return this.relocateTmp(publicPath, destRelDir, 'copy');
  }

  async rewriteDesignAssets(
    shopId: string,
    design: unknown,
    dest: { bgRelDir: string; stickerRelDir: string },
    opts?: { mode?: 'move' | 'copy' },
  ): Promise<unknown> {
    if (!design || typeof design !== 'object') return design;
    const d: any = Array.isArray(design) ? [...design] : { ...(design as any) };
    const mode = opts?.mode ?? 'move';

    if (
      typeof d.bgImage === 'string' &&
      d.bgImage.startsWith('/uploads/shops/') &&
      d.bgImage.includes('/tmp/')
    ) {
      d.bgImage =
        mode === 'copy'
          ? await this.copyIfTmp(d.bgImage, dest.bgRelDir)
          : await this.moveIfTmp(d.bgImage, dest.bgRelDir);
    }

    if (Array.isArray(d.elements)) {
      d.elements = await Promise.all(
        d.elements.map(async (el: any) => {
          if (el && el.type === 'image' && typeof el.imageSrc === 'string') {
            if (
              el.imageSrc.startsWith('/uploads/shops/') &&
              el.imageSrc.includes('/tmp/')
            ) {
              const next =
                mode === 'copy'
                  ? await this.copyIfTmp(el.imageSrc, dest.stickerRelDir)
                  : await this.moveIfTmp(el.imageSrc, dest.stickerRelDir);
              return { ...el, imageSrc: next };
            }
          }
          return el;
        }),
      );
    }
    return d;
  }
}
