import {
  BadRequestException,
  ForbiddenException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import {
  Prisma,
  ReviewCashiStatus,
  ReviewGoogleStatus,
  ReviewSource,
  ShopStatus,
  UserRole,
} from '@prisma/client';
import { PrismaService } from '../../database/prisma.service';
import { CreateReviewDto } from './dto/create-review.dto';
import { CreatePublicReviewDto } from './dto/create-public-review.dto';
import { PlanLimitException } from '../plans/plan-limit.exception';
import { supportsGoogleReviews } from '../plans/plan-limits';

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

const reviewSelect = {
  id: true,
  shopId: true,
  customerUserId: true,
  source: true,
  category: true,
  customerName: true,
  customerPhone: true,
  rating: true,
  comment: true,
  googleStatus: true,
  googleExternalId: true,
  googlePostDate: true,
  cashiStatus: true,
  cashiPostDate: true,
  response: true,
  createdAt: true,
} as const;

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

  private normalizeCategory(raw: string) {
    const category = String(raw ?? '')
      .trim()
      .replace(/\s+/g, ' ');
    if (!category) throw new BadRequestException('category is required');
    if (category.length > 120)
      throw new BadRequestException('category must be 120 characters or less');
    return category;
  }

  private async getPublicShop(shopId: string) {
    const id = String(shopId ?? '').trim();
    if (!id) throw new BadRequestException('shopId is required');

    const shop = await this.prisma.shop.findFirst({
      where: { id, status: ShopStatus.APPROVED, isActive: true },
      select: {
        id: true,
        name: true,
        city: true,
        state: true,
        imageUrl: true,
      },
    });
    if (!shop) throw new NotFoundException('Shop not found');
    return shop;
  }

  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;
    }
    if (!user.shopId || user.shopId !== shopId)
      throw new ForbiddenException('Forbidden');
  }

  async list(user: RequestUser, shopIdQuery?: string) {
    const shopId =
      user.role === UserRole.SUPERADMIN
        ? (shopIdQuery ?? null)
        : (user.shopId ?? null);
    if (!shopId) throw new ForbiddenException('No shop selected');
    await this.assertCanAccessShop(user, shopId);

    return this.prisma.review.findMany({
      where: { shopId },
      orderBy: { createdAt: 'desc' },
      select: reviewSelect,
    });
  }

  async listPaged(
    user: RequestUser,
    args: {
      page: number;
      limit: number;
      q?: string;
      rating?: string;
      googleStatus?: string;
      cashiStatus?: string;
      source?: string;
      category?: string;
      from?: string;
      to?: 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 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);

    const q = String(args.q ?? '').trim();
    const ratingRaw = args.rating != null ? String(args.rating).trim() : '';
    const googleRaw =
      args.googleStatus != null
        ? String(args.googleStatus).trim().toUpperCase()
        : '';
    const cashiRaw =
      args.cashiStatus != null
        ? String(args.cashiStatus).trim().toUpperCase()
        : '';
    const sourceRaw =
      args.source != null ? String(args.source).trim().toUpperCase() : '';
    const categoryRaw = String(args.category ?? '').trim();

    const parseDate = (s?: string) => {
      const raw = (s ?? '').trim();
      if (!raw) return null;
      const d = new Date(raw);
      if (Number.isNaN(d.getTime()))
        throw new BadRequestException('Invalid date');
      return d;
    };
    const from = parseDate(args.from);
    const to = parseDate(args.to);

    let rating: number | undefined;
    if (ratingRaw && /^\d+$/.test(ratingRaw)) {
      const n = Number(ratingRaw);
      if (n >= 1 && n <= 5) rating = n;
    }

    let googleStatus: ReviewGoogleStatus | undefined;
    if (googleRaw === 'NOT_POSTED')
      googleStatus = ReviewGoogleStatus.NOT_POSTED;
    else if (googleRaw === 'POSTED') googleStatus = ReviewGoogleStatus.POSTED;
    else if (googleRaw === 'REMOVED') googleStatus = ReviewGoogleStatus.REMOVED;

    let cashiStatus: ReviewCashiStatus | undefined;
    if (cashiRaw === 'NOT_POSTED') cashiStatus = ReviewCashiStatus.NOT_POSTED;
    else if (cashiRaw === 'POSTED') cashiStatus = ReviewCashiStatus.POSTED;
    else if (cashiRaw === 'REMOVED') cashiStatus = ReviewCashiStatus.REMOVED;

    let source: ReviewSource | undefined;
    if (sourceRaw === 'CASHI') source = ReviewSource.CASHI;
    else if (sourceRaw === 'GOOGLE') source = ReviewSource.GOOGLE;

    const where: Prisma.ReviewWhereInput = { shopId };
    if (rating != null) where.rating = rating;
    if (googleStatus) where.googleStatus = googleStatus;
    if (cashiStatus) where.cashiStatus = cashiStatus;
    if (source) where.source = source;
    if (categoryRaw) {
      where.category = { contains: categoryRaw, mode: 'insensitive' };
    }
    if (from || to) {
      where.createdAt = {
        ...(from ? { gte: from } : {}),
        ...(to ? { lte: to } : {}),
      };
    }
    if (q) {
      where.OR = [
        { id: { contains: q, mode: 'insensitive' } },
        { customerName: { contains: q, mode: 'insensitive' } },
        { customerPhone: { contains: q, mode: 'insensitive' } },
        { category: { contains: q, mode: 'insensitive' } },
        { comment: { contains: q, mode: 'insensitive' } },
        { response: { contains: q, mode: 'insensitive' } },
      ];
    }

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

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

  async create(user: RequestUser, dto: CreateReviewDto) {
    const shopId = user.shopId ?? null;
    if (!shopId) throw new ForbiddenException('No shop selected');
    await this.assertCanAccessShop(user, shopId);

    if (dto.rating < 1 || dto.rating > 5)
      throw new BadRequestException('rating must be between 1 and 5');

    return this.prisma.review.create({
      data: {
        shopId,
        customerUserId: null,
        source: ReviewSource.CASHI,
        category: null,
        customerName: dto.customerName.trim(),
        customerPhone: dto.customerPhone?.trim()
          ? dto.customerPhone.trim()
          : null,
        rating: Math.floor(dto.rating),
        comment: dto.comment.trim(),
        googleStatus: ReviewGoogleStatus.NOT_POSTED,
        cashiStatus: ReviewCashiStatus.NOT_POSTED,
      },
      select: reviewSelect,
    });
  }

  async getPublicContext(shopId: string, categoryRaw: string) {
    const shop = await this.getPublicShop(shopId);
    return {
      shopId: shop.id,
      category: this.normalizeCategory(categoryRaw),
      shop: {
        id: shop.id,
        name: shop.name,
        city: shop.city,
        state: shop.state,
        imageUrl: shop.imageUrl,
      },
    };
  }

  async createPublic(dto: CreatePublicReviewDto) {
    if (dto.rating < 1 || dto.rating > 5) {
      throw new BadRequestException('rating must be between 1 and 5');
    }

    const shop = await this.getPublicShop(dto.shopId);
    const category = this.normalizeCategory(dto.category);
    const customerUserId = String(dto.customerUserId ?? '').trim() || null;

    const linkedUser = customerUserId
      ? await this.prisma.user.findUnique({
          where: { id: customerUserId },
          select: { id: true, name: true, phone: true, isActive: true },
        })
      : null;

    if (customerUserId && !linkedUser) {
      throw new BadRequestException('customerUserId is invalid');
    }
    if (linkedUser && !linkedUser.isActive) {
      throw new BadRequestException('Linked customer account is inactive');
    }

    const customerName =
      String(dto.customerName ?? '').trim() || linkedUser?.name?.trim() || '';
    if (!customerName) {
      throw new BadRequestException('customerName is required');
    }

    const comment = String(dto.comment ?? '').trim();
    if (!comment) throw new BadRequestException('comment is required');

    const customerPhone =
      String(dto.customerPhone ?? '').trim() || linkedUser?.phone?.trim() || null;

    return this.prisma.review.create({
      data: {
        shopId: shop.id,
        customerUserId: linkedUser?.id ?? null,
        source: ReviewSource.CASHI,
        category,
        customerName,
        customerPhone,
        rating: Math.floor(dto.rating),
        comment,
        googleStatus: ReviewGoogleStatus.NOT_POSTED,
        cashiStatus: ReviewCashiStatus.NOT_POSTED,
      },
      select: reviewSelect,
    });
  }

  async respond(user: RequestUser, reviewId: string, response: string) {
    const review = await this.prisma.review.findUnique({
      where: { id: reviewId },
      select: { id: true, shopId: true },
    });
    if (!review) throw new NotFoundException('Review not found');
    await this.assertCanAccessShop(user, review.shopId);

    return this.prisma.review.update({
      where: { id: review.id },
      data: { response: response.trim() },
      select: { id: true, response: true, updatedAt: true },
    });
  }

  async delete(user: RequestUser, reviewId: string) {
    const review = await this.prisma.review.findUnique({
      where: { id: reviewId },
      select: { id: true, shopId: true },
    });
    if (!review) throw new NotFoundException('Review not found');
    await this.assertCanAccessShop(user, review.shopId);
    await this.prisma.review.delete({ where: { id: review.id } });
    return { deleted: true };
  }

  private async assertGoogleConfigured(shopId: string) {
    const shop = await this.prisma.shop.findUnique({
      where: { id: shopId },
      select: { googleApiKey: true, googlePlaceId: true },
    });
    if (!shop) throw new NotFoundException('Shop not found');
    if (!shop.googleApiKey || !shop.googlePlaceId) {
      throw new BadRequestException(
        'Google integration is not configured for this shop',
      );
    }
  }

  async postToGoogle(user: RequestUser, reviewId: string) {
    const review = await this.prisma.review.findUnique({
      where: { id: reviewId },
      select: { id: true, shopId: true, googleStatus: true },
    });
    if (!review) throw new NotFoundException('Review not found');
    await this.assertCanAccessShop(user, review.shopId);

    const shop = await this.prisma.shop.findUnique({
      where: { id: review.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');
    if (!supportsGoogleReviews(sharedPlan.features)) {
      throw new PlanLimitException({
        featureKey: 'Reviews',
        currentPlanCode: sharedPlan.code,
        limit: 0,
        used: 0,
        period: 'ALWAYS',
        message: 'Upgrade plan to use Google reviews',
      });
    }
    await this.assertGoogleConfigured(review.shopId);

    if (review.googleStatus === ReviewGoogleStatus.POSTED)
      return { posted: true };

    const updated = await this.prisma.review.update({
      where: { id: review.id },
      data: {
        googleStatus: ReviewGoogleStatus.POSTED,
        googlePostDate: new Date(),
        // googleExternalId: set when real integration is connected
      },
      select: { id: true, googleStatus: true, googlePostDate: true },
    });
    return { posted: true, review: updated };
  }

  async removeFromGoogle(user: RequestUser, reviewId: string) {
    const review = await this.prisma.review.findUnique({
      where: { id: reviewId },
      select: { id: true, shopId: true },
    });
    if (!review) throw new NotFoundException('Review not found');
    await this.assertCanAccessShop(user, review.shopId);

    const shop = await this.prisma.shop.findUnique({
      where: { id: review.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');
    if (!supportsGoogleReviews(sharedPlan.features)) {
      throw new PlanLimitException({
        featureKey: 'Reviews',
        currentPlanCode: sharedPlan.code,
        limit: 0,
        used: 0,
        period: 'ALWAYS',
        message: 'Upgrade plan to use Google reviews',
      });
    }
    await this.assertGoogleConfigured(review.shopId);

    const updated = await this.prisma.review.update({
      where: { id: review.id },
      data: {
        googleStatus: ReviewGoogleStatus.REMOVED,
      },
      select: { id: true, googleStatus: true },
    });
    return { removed: true, review: updated };
  }

  async postToCashi(user: RequestUser, reviewId: string) {
    const review = await this.prisma.review.findUnique({
      where: { id: reviewId },
      select: { id: true, shopId: true, cashiStatus: true },
    });
    if (!review) throw new NotFoundException('Review not found');
    await this.assertCanAccessShop(user, review.shopId);

    if (review.cashiStatus === ReviewCashiStatus.POSTED)
      return { posted: true };

    const updated = await this.prisma.review.update({
      where: { id: review.id },
      data: {
        cashiStatus: ReviewCashiStatus.POSTED,
        cashiPostDate: new Date(),
      },
      select: { id: true, cashiStatus: true, cashiPostDate: true },
    });
    return { posted: true, review: updated };
  }

  async removeFromCashi(user: RequestUser, reviewId: string) {
    const review = await this.prisma.review.findUnique({
      where: { id: reviewId },
      select: { id: true, shopId: true },
    });
    if (!review) throw new NotFoundException('Review not found');
    await this.assertCanAccessShop(user, review.shopId);

    const updated = await this.prisma.review.update({
      where: { id: review.id },
      data: { cashiStatus: ReviewCashiStatus.REMOVED },
      select: { id: true, cashiStatus: true },
    });
    return { removed: true, review: updated };
  }
}
