import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
import { AccountStatus, UserRole, WalletType } from '@prisma/client';
import { Prisma } from '@prisma/client';
import bcrypt from 'bcrypt';
import { PrismaService } from '../database/prisma.service';
import { MapsService } from '../maps/maps.service';
import { UpdateMyRestaurantDto } from './dto/update-my-restaurant.dto';
import { UpsertRestaurantDto } from './dto/upsert-restaurant.dto';

@Injectable()
export class RestaurantsService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly mapsService: MapsService,
  ) {}

  list() {
    return this.prisma.restaurant.findMany({
      orderBy: { createdAt: 'desc' },
      include: {
        user: { select: { id: true, email: true, phone: true, role: true, status: true, createdAt: true, updatedAt: true } },
        wallet: true,
        reviews: { select: { rating: true } },
        _count: { select: { orders: true } },
      },
    });
  }

  getByUserId(userId: string) {
    return this.prisma.restaurant.findUniqueOrThrow({
      where: { userId },
      include: {
        user: { select: { id: true, email: true, phone: true, role: true, status: true, createdAt: true, updatedAt: true } },
        wallet: true,
        _count: { select: { orders: true, products: true, promotions: true } },
        reviews: { select: { rating: true } },
      },
    });
  }

  async create(data: UpsertRestaurantDto) {
    try {
      const passwordHash = await bcrypt.hash(data.password, 12);

      return await this.prisma.$transaction(async (tx) => {
        const user = await tx.user.create({
          data: {
            email: data.ownerEmail.trim().toLowerCase(),
            phone: data.ownerPhone.trim(),
            passwordHash,
            role: UserRole.RESTAURANT,
            status: data.status ?? AccountStatus.PENDING,
            emailVerifiedAt: data.status === AccountStatus.ACTIVE ? new Date() : undefined,
            phoneVerifiedAt: data.status === AccountStatus.ACTIVE ? new Date() : undefined,
          },
        });

        const resolvedCoordinates = await this.resolveRestaurantCoordinatesFromPayload(data);

        const restaurant = await tx.restaurant.create({
          data: {
            userId: user.id,
            name: data.name.trim(),
            slug: this.normalizeSlug(data.slug),
            documentNumber: data.documentNumber.trim(),
            description: data.description?.trim(),
            pixKey: data.pixKey?.trim(),
            street: data.street?.trim(),
            number: data.number?.trim(),
            complement: data.complement?.trim(),
            city: data.city?.trim(),
            neighborhood: data.neighborhood?.trim(),
            state: data.state?.trim(),
            zipCode: data.zipCode?.trim(),
            latitude: resolvedCoordinates?.latitude,
            longitude: resolvedCoordinates?.longitude,
            acceptsCashOnDelivery: data.acceptsCashOnDelivery ?? false,
            commissionPercentage: data.commissionPercentage ? Number(data.commissionPercentage) : undefined,
            companyContractUrl: data.companyContractUrl?.trim(),
            businessLicenseUrl: data.businessLicenseUrl?.trim(),
            ownerDocumentUrl: data.ownerDocumentUrl?.trim(),
            bankProofUrl: data.bankProofUrl?.trim(),
            coverImageUrl: data.coverImageUrl?.trim(),
            logoUrl: data.logoUrl?.trim(),
            documentReviewNotes: data.documentReviewNotes?.trim(),
            documentsSubmittedAt: this.resolveDocumentsSubmittedAt(data),
            approvedAt: data.status === AccountStatus.ACTIVE ? new Date() : undefined,
            rejectedAt: data.status === AccountStatus.REJECTED ? new Date() : undefined,
            status: data.status ?? AccountStatus.PENDING,
          },
        });

        await tx.wallet.create({
          data: {
            userId: user.id,
            restaurantId: restaurant.id,
            type: WalletType.RESTAURANT,
            balance: 0,
          },
        });

        return tx.restaurant.findUniqueOrThrow({
          where: { id: restaurant.id },
          include: {
            user: { select: { id: true, email: true, phone: true, role: true, status: true, createdAt: true, updatedAt: true } },
            wallet: true,
            _count: { select: { orders: true } },
            reviews: { select: { rating: true } },
          },
        });
      });
    } catch (error) {
      this.handlePersistenceError(error);
    }
  }

  async update(id: string, data: Partial<UpsertRestaurantDto>) {
    const current = await this.prisma.restaurant.findUnique({ where: { id }, include: { user: true } });
    if (!current) {
      throw new NotFoundException('Restaurante nao encontrado.');
    }

    const userUpdate: Record<string, unknown> = {};
    if (data.ownerEmail) userUpdate.email = data.ownerEmail.trim().toLowerCase();
    if (data.ownerPhone) userUpdate.phone = data.ownerPhone.trim();
    if (data.password) userUpdate.passwordHash = await bcrypt.hash(data.password, 12);
    if (data.status) userUpdate.status = data.status;

    try {
      return await this.prisma.$transaction(async (tx) => {
        if (Object.keys(userUpdate).length) {
          await tx.user.update({ where: { id: current.userId }, data: userUpdate });
        }

        const resolvedCoordinates = await this.resolveRestaurantCoordinatesFromPayload(data, current);

        return tx.restaurant.update({
          where: { id },
          data: {
            name: data.name?.trim(),
            slug: data.slug ? this.normalizeSlug(data.slug) : undefined,
            documentNumber: data.documentNumber?.trim(),
            description: data.description?.trim(),
            pixKey: data.pixKey?.trim(),
            street: data.street?.trim(),
            number: data.number?.trim(),
            complement: data.complement?.trim(),
            city: data.city?.trim(),
            neighborhood: data.neighborhood?.trim(),
            state: data.state?.trim(),
            zipCode: data.zipCode?.trim(),
            latitude: resolvedCoordinates?.latitude,
            longitude: resolvedCoordinates?.longitude,
            acceptsCashOnDelivery: data.acceptsCashOnDelivery,
            commissionPercentage: data.commissionPercentage ? Number(data.commissionPercentage) : undefined,
            companyContractUrl: data.companyContractUrl?.trim(),
            businessLicenseUrl: data.businessLicenseUrl?.trim(),
            ownerDocumentUrl: data.ownerDocumentUrl?.trim(),
            bankProofUrl: data.bankProofUrl?.trim(),
            coverImageUrl: data.coverImageUrl?.trim(),
            logoUrl: data.logoUrl?.trim(),
            documentReviewNotes: data.documentReviewNotes?.trim(),
            documentsSubmittedAt: this.resolveDocumentsSubmittedAt(data, current.documentsSubmittedAt),
            approvedAt: data.status === AccountStatus.ACTIVE ? new Date() : data.status ? null : undefined,
            rejectedAt: data.status === AccountStatus.REJECTED ? new Date() : data.status === AccountStatus.ACTIVE ? null : undefined,
            status: data.status,
          },
          include: {
            user: { select: { id: true, email: true, phone: true, role: true, status: true, createdAt: true, updatedAt: true } },
            wallet: true,
            _count: { select: { orders: true } },
            reviews: { select: { rating: true } },
          },
        });
      });
    } catch (error) {
      this.handlePersistenceError(error);
    }
  }

  async archive(id: string) {
    const current = await this.prisma.restaurant.findUnique({ where: { id } });
    if (!current) {
      throw new NotFoundException('Restaurante nao encontrado.');
    }

    return this.prisma.$transaction(async (tx) => {
      await tx.user.update({
        where: { id: current.userId },
        data: { status: AccountStatus.SUSPENDED },
      });

      return tx.restaurant.update({
        where: { id },
        data: {
          status: AccountStatus.SUSPENDED,
          isOpen: false,
          isPaused: true,
        },
      });
    });
  }

  async updateByUserId(userId: string, data: UpdateMyRestaurantDto) {
    const current = await this.prisma.restaurant.findUnique({
      where: { userId },
      include: { user: true },
    });
    if (!current) {
      throw new NotFoundException('Perfil da loja nao encontrado.');
    }

    const userUpdate: Record<string, unknown> = {};
    if (data.ownerEmail) userUpdate.email = data.ownerEmail.trim().toLowerCase();
    if (data.ownerPhone) userUpdate.phone = data.ownerPhone.trim();
    if (data.password) userUpdate.passwordHash = await bcrypt.hash(data.password, 12);

    try {
      return await this.prisma.$transaction(async (tx) => {
        if (Object.keys(userUpdate).length) {
          await tx.user.update({ where: { id: current.userId }, data: userUpdate });
        }

        const resolvedCoordinates = await this.resolveRestaurantCoordinatesFromPayload(data, current);

        return tx.restaurant.update({
          where: { userId },
          data: {
            name: data.name?.trim(),
            slug: data.slug ? this.normalizeSlug(data.slug) : undefined,
            documentNumber: data.documentNumber?.trim(),
            description: data.description?.trim(),
            pixKey: data.pixKey?.trim(),
            street: data.street?.trim(),
            number: data.number?.trim(),
            complement: data.complement?.trim(),
            city: data.city?.trim(),
            neighborhood: data.neighborhood?.trim(),
            state: data.state?.trim(),
            zipCode: data.zipCode?.trim(),
            latitude: resolvedCoordinates?.latitude,
            longitude: resolvedCoordinates?.longitude,
            acceptsCashOnDelivery: data.acceptsCashOnDelivery,
            companyContractUrl: data.companyContractUrl?.trim(),
            businessLicenseUrl: data.businessLicenseUrl?.trim(),
            ownerDocumentUrl: data.ownerDocumentUrl?.trim(),
            bankProofUrl: data.bankProofUrl?.trim(),
            documentsSubmittedAt: this.resolveDocumentsSubmittedAt(data, current.documentsSubmittedAt),
            commissionPercentage: data.commissionPercentage ? Number(data.commissionPercentage) : undefined,
            serviceFeePercentage: data.serviceFeePercentage ? Number(data.serviceFeePercentage) : undefined,
            baseDeliveryFee: data.baseDeliveryFee ? Number(data.baseDeliveryFee) : undefined,
            pricePerKm: data.pricePerKm ? Number(data.pricePerKm) : undefined,
            isOpen: data.isOpen,
            isPaused: data.isPaused,
            coverImageUrl: data.coverImageUrl?.trim(),
            logoUrl: data.logoUrl?.trim(),
          },
          include: {
            user: { select: { id: true, email: true, phone: true, role: true, status: true, createdAt: true, updatedAt: true } },
            wallet: true,
            _count: { select: { orders: true, products: true, promotions: true } },
            reviews: { select: { rating: true } },
          },
        });
      });
    } catch (error) {
      this.handlePersistenceError(error);
    }
  }

  async assignUploadedDocument(
    userId: string,
    field: 'companyContractUrl' | 'businessLicenseUrl' | 'ownerDocumentUrl' | 'bankProofUrl' | 'logoUrl' | 'coverImageUrl',
    url: string,
  ) {
    const current = await this.prisma.restaurant.findUnique({
      where: { userId },
      select: {
        documentsSubmittedAt: true,
      },
    });
    if (!current) {
      throw new NotFoundException('Perfil da loja nao encontrado.');
    }

    return this.prisma.restaurant.update({
      where: { userId },
      data: {
        [field]: url,
        documentsSubmittedAt:
          field === 'logoUrl' || field === 'coverImageUrl'
            ? current.documentsSubmittedAt ?? undefined
            : current.documentsSubmittedAt ?? new Date(),
      },
      include: {
        user: { select: { id: true, email: true, phone: true, role: true, status: true, createdAt: true, updatedAt: true } },
        wallet: true,
        _count: { select: { orders: true, products: true, promotions: true } },
        reviews: { select: { rating: true } },
      },
    });
  }

  private normalizeSlug(value: string) {
    return value
      .trim()
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, '-')
      .replace(/^-+|-+$/g, '');
  }

  private resolveDocumentsSubmittedAt(
    data: Partial<UpsertRestaurantDto & UpdateMyRestaurantDto>,
    currentValue?: Date | null,
  ) {
    const hasDocumentPayload = [
      data.companyContractUrl,
      data.businessLicenseUrl,
      data.ownerDocumentUrl,
      data.bankProofUrl,
    ].some((value) => Boolean(value?.trim()));

    if (hasDocumentPayload) {
      return currentValue ?? new Date();
    }

    return currentValue ?? undefined;
  }

  private async resolveRestaurantCoordinatesFromPayload(
    data: Partial<UpsertRestaurantDto & UpdateMyRestaurantDto>,
    current?: {
      street?: string | null;
      number?: string | null;
      complement?: string | null;
      neighborhood?: string | null;
      city?: string | null;
      state?: string | null;
      zipCode?: string | null;
      latitude?: Prisma.Decimal | number | string | null;
      longitude?: Prisma.Decimal | number | string | null;
    },
  ) {
    const manualLatitude =
      data.latitude !== undefined
        ? data.latitude
          ? Number(data.latitude)
          : null
        : current?.latitude !== undefined && current?.latitude !== null
          ? Number(current.latitude)
          : null;
    const manualLongitude =
      data.longitude !== undefined
        ? data.longitude
          ? Number(data.longitude)
          : null
        : current?.longitude !== undefined && current?.longitude !== null
          ? Number(current.longitude)
          : null;

    if (Number.isFinite(manualLatitude) && Number.isFinite(manualLongitude)) {
      return { latitude: manualLatitude as number, longitude: manualLongitude as number };
    }

    const addressString = [
      [data.street?.trim() ?? current?.street, data.number?.trim() ?? current?.number].filter(Boolean).join(', '),
      data.complement?.trim() ?? current?.complement,
      data.neighborhood?.trim() ?? current?.neighborhood,
      data.city?.trim() ?? current?.city,
      data.state?.trim() ?? current?.state,
      data.zipCode?.trim() ?? current?.zipCode,
    ]
      .filter(Boolean)
      .join(', ');

    if (!addressString) {
      return {
        latitude: manualLatitude,
        longitude: manualLongitude,
      };
    }

    const geocoded = await this.mapsService.geocodeAddressStrict({
      street: data.street?.trim() ?? current?.street,
      number: data.number?.trim() ?? current?.number,
      complement: data.complement?.trim() ?? current?.complement,
      neighborhood: data.neighborhood?.trim() ?? current?.neighborhood,
      city: data.city?.trim() ?? current?.city,
      state: data.state?.trim() ?? current?.state,
      zipCode: data.zipCode?.trim() ?? current?.zipCode,
    });
    if (geocoded) {
      return {
        latitude: geocoded.latitude,
        longitude: geocoded.longitude,
      };
    }

    return {
      latitude: manualLatitude,
      longitude: manualLongitude,
    };
  }

  private handlePersistenceError(error: unknown): never {
    if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
      const target = Array.isArray(error.meta?.target) ? error.meta?.target.join(', ') : String(error.meta?.target ?? '');
      if (target.includes('email')) {
        throw new ConflictException('Ja existe um restaurante com este email responsavel.');
      }
      if (target.includes('phone')) {
        throw new ConflictException('Ja existe um restaurante com este telefone responsavel.');
      }
      if (target.includes('slug')) {
        throw new ConflictException('Este slug ja esta em uso. Escolha outro identificador para a loja.');
      }
      if (target.includes('documentNumber')) {
        throw new ConflictException('Este CNPJ/documento ja esta cadastrado.');
      }
      throw new ConflictException('Ja existe um registro de restaurante com estes dados.');
    }

    throw error;
  }
}
