"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.NumberPlateService = void 0;
const common_1 = require("@nestjs/common");
const config_1 = require("@nestjs/config");
const typeorm_1 = require("@nestjs/typeorm");
const axios_1 = require("axios");
const fs_1 = require("fs");
const path_1 = require("path");
const typeorm_2 = require("typeorm");
const numberplate_listing_entity_1 = require("../../common/numberplate/numberplate-listing.entity");
const marketplace_listing_status_enum_1 = require("../../common/marketplace/marketplace-listing-status.enum");
const plateConfig = {
    dubai: {
        label: "Dubai",
        designs: {
            normal: {
                label: "Normal",
                slug: "dubai",
                codes: [
                    "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
                    "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
                    "AA", "BB", "CC", "DD", "EE", "FF",
                ],
            },
            classic: {
                label: "Classic",
                slug: "dubai-classic",
                codes: [
                    "NOC",
                    "A",
                    "B",
                    "C",
                    "D",
                    "E",
                    "F",
                    "G",
                    "H",
                    "I",
                    "J",
                    "K",
                    "L",
                    "M",
                    "N",
                    "O",
                    "P",
                    "Q",
                    "R",
                    "S",
                    "T",
                    "U",
                    "V",
                    "W",
                    "X",
                    "Y",
                    "Z",
                ],
            },
        },
    },
    "abu-dhabi": {
        label: "Abu Dhabi",
        designs: {
            normal: {
                label: "Normal",
                slug: "abu-dhabi",
                codes: ["1", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "50"],
            },
            classic: {
                label: "Classic",
                slug: "abu-dhabi-classic",
                codes: ["NOC", "AD", "AUH", "VIP", "CLASSIC", "1", "2", "3", "4", "5"],
            },
        },
    },
    sharjah: {
        label: "Sharjah",
        designs: {
            normal: {
                label: "Normal",
                slug: "sharjah",
                codes: ["1", "2", "3"],
            },
        },
    },
    ajman: {
        label: "Ajman",
        designs: {
            normal: {
                label: "Normal",
                slug: "ajman",
                codes: ["A", "B", "C", "D", "E", "H"],
            },
        },
    },
    fujairah: {
        label: "Fujairah",
        designs: {
            normal: {
                label: "Normal",
                slug: "fujairah",
                codes: ["A", "B", "C", "D", "E", "F", "G", "K", "M", "P", "R", "S", "T"],
            },
        },
    },
    "ras-al-khaimah": {
        label: "Ras Al Khaimah",
        designs: {
            normal: {
                label: "Normal",
                slug: "ras-al-khaimah",
                codes: ["A", "C", "D", "I", "K", "M", "N", "S", "V", "Y"],
            },
        },
    },
    "umm-al-quwain": {
        label: "Umm Al Quwain",
        designs: {
            normal: {
                label: "Normal",
                slug: "umm-al-quwain",
                codes: ["A", "B", "C", "D", "E", "F", "G", "H", "I", "M", "X"],
            },
        },
    },
};
const bikePlateConfig = {
    "abu-dhabi": {
        label: "Abu Dhabi",
        designs: {
            normal: { label: "Normal", slug: "adnb" },
        },
    },
    dubai: {
        label: "Dubai",
        designs: {
            normal: { label: "Normal", slug: "dxbnb" },
        },
    },
    sharjah: {
        label: "Sharjah",
        designs: {
            normal: { label: "Normal", slug: "shjnb" },
        },
    },
    ajman: {
        label: "Ajman",
        designs: {
            normal: { label: "Normal", slug: "ajmnb" },
        },
    },
    fujairah: {
        label: "Fujairah",
        designs: {
            normal: { label: "Normal", slug: "fujnb" },
        },
    },
    "ras-al-khaimah": {
        label: "Ras Al Khaimah",
        designs: {
            normal: { label: "Normal", slug: "raknb" },
        },
    },
    "umm-al-quwain": {
        label: "Umm Al Quwain",
        designs: {
            normal: { label: "Normal", slug: "umqnb" },
        },
    },
};
const DEFAULT_CAR_SLUG = "mercedes-benz-g";
const carAssets = {
    "mercedes-benz-g": {
        label: "Mercedes-Benz G",
        front: {
            path: (0, path_1.join)(process.cwd(), "src", "assets", "mercedes-benz-g-front.webp"),
            width: 933,
            height: 700,
            detection: { x: 464, y: 478, width: 164, height: 37, confidence: 0.8906, class: "License_Plate" },
        },
        back: {
            path: (0, path_1.join)(process.cwd(), "src", "assets", "mercedes-benz-g-back.webp"),
            width: 933,
            height: 700,
            detection: { x: 479, y: 478, width: 141, height: 32, confidence: 0.9297, class: "License_Plate" },
        },
    },
    "lamborghini-urus": {
        label: "Lamborghini Urus",
        front: {
            path: (0, path_1.join)(process.cwd(), "src", "assets", "lamborghini-urus-front.jpeg"),
            width: 1536,
            height: 1024,
            detection: { x: 767, y: 717, width: 209, height: 45, confidence: 0.7813, class: "License_Plate" },
        },
        back: {
            path: (0, path_1.join)(process.cwd(), "src", "assets", "lamborghini-urus-back.jpeg"),
            width: 1280,
            height: 853,
            detection: { x: 635, y: 446, width: 194, height: 40, confidence: 0.7422, class: "License_Plate" },
        },
    },
    "mercedes-amg-g63": {
        label: "Mercedes-AMG G63",
        front: {
            path: (0, path_1.join)(process.cwd(), "src", "assets", "mercedes-amg-g63-front.jpeg"),
            width: 1536,
            height: 1024,
            detection: { x: 761, y: 611, width: 241, height: 50, confidence: 0.8086, class: "License_Plate" },
        },
        back: {
            path: (0, path_1.join)(process.cwd(), "src", "assets", "mercedes-amg-g63-back.jpeg"),
            width: 1280,
            height: 853,
            detection: { x: 637, y: 569, width: 161, height: 38, confidence: 0.6992, class: "License_Plate" },
        },
    },
    "mercedes-amg-gt-r": {
        label: "Mercedes-AMG GT R",
        front: {
            path: (0, path_1.join)(process.cwd(), "src", "assets", "mercedes-amg-gt-r-front.jpeg"),
            width: 1536,
            height: 1024,
            detection: { x: 767, y: 719, width: 199, height: 43, confidence: 0.7813, class: "License_Plate" },
        },
        back: {
            path: (0, path_1.join)(process.cwd(), "src", "assets", "mercedes-amg-gt-r-back.jpeg"),
            width: 1280,
            height: 853,
            detection: { x: 640, y: 537, width: 223, height: 48, confidence: 0.6328, class: "License_Plate" },
        },
    },
    "porsche-911-gt3-rs": {
        label: "Porsche 911 GT3 RS",
        front: {
            path: (0, path_1.join)(process.cwd(), "src", "assets", "porsche-911-gt3-rs-front.jpeg"),
            width: 1536,
            height: 1024,
            detection: { x: 766, y: 689, width: 251, height: 54, confidence: 0.7109, class: "License_Plate" },
        },
        back: {
            path: (0, path_1.join)(process.cwd(), "src", "assets", "porsche-911-gt3-rs-back.jpeg"),
            width: 1280,
            height: 853,
            detection: { x: 640, y: 513, width: 197, height: 43, confidence: 0.7266, class: "License_Plate" },
        },
    },
    "kawasaki": {
        label: "Kawasaki",
        back: {
            path: (0, path_1.join)(process.cwd(), "src", "assets", "kawasaki-back.jpeg"),
            width: 1280,
            height: 960,
            detection: { x: 631, y: 519, width: 156, height: 95, confidence: 0.8555, class: "License_Plate" },
        },
    },
    "slingshot-s": {
        label: "Slingshot S",
        front: {
            path: (0, path_1.join)(process.cwd(), "src", "assets", "slingshot-s-front.jpeg"),
            width: 1280,
            height: 853,
            detection: { x: 634, y: 577, width: 176, height: 78, confidence: 0.7188, class: "License_Plate" },
        },
        back: {
            path: (0, path_1.join)(process.cwd(), "src", "assets", "slingshot-s-back.jpeg"),
            width: 1280,
            height: 853,
            detection: { x: 636, y: 480, width: 141, height: 86, confidence: 0.7891, class: "License_Plate" },
        },
    },
};
let NumberPlateService = class NumberPlateService {
    constructor(configService, listingRepository) {
        this.configService = configService;
        this.listingRepository = listingRepository;
    }
    onModuleInit() {
        this.startExpiryCleanupLoop();
    }
    onModuleDestroy() {
        if (this.expiryCleanupTimer) {
            clearInterval(this.expiryCleanupTimer);
        }
    }
    getConfig() {
        return plateConfig;
    }
    getCarAssets() {
        return Object.entries(carAssets).map(([slug, entry]) => ({ slug, label: entry.label }));
    }
    async generateAllCarPreviews(query) {
        const results = {};
        for (const slug of Object.keys(carAssets)) {
            results[slug] = await this.generateCarPreview({ ...query, carSlug: slug });
        }
        return results;
    }
    decorateListing(listing) {
        return {
            ...listing,
            contact: {
                whatsappUrl: this.buildWhatsappUrl(listing.sellerMobile, listing),
                whatsappText: this.buildWhatsappText(listing),
            },
            isExpired: listing.expiresAt ? listing.expiresAt <= new Date() : false,
        };
    }
    async createListing(dto, authenticatedUserId) {
        await this.cleanupExpiredListings();
        const vehicleType = dto.vehicleType || "car";
        const preview = this.generatePreview({
            vehicleType,
            emirate: dto.emirate,
            design: dto.design,
            code: dto.code,
            number: dto.number,
            flag1: dto.flag1 || "0",
            flag2: dto.flag2 || "0",
            width: "1200",
        });
        let frontPreviewUrl = null;
        let backPreviewUrl = null;
        try {
            const carSlug = dto.carSlug || (vehicleType === "bike" ? "kawasaki" : DEFAULT_CAR_SLUG);
            const carPreview = await this.generateCarPreview({
                plateImageUrl: preview.imageUrl,
                carSlug,
                publicBaseUrl: dto.publicBaseUrl,
            });
            frontPreviewUrl = carPreview.front?.previewUrl || null;
            backPreviewUrl = carPreview.back?.previewUrl || null;
        }
        catch (error) {
            frontPreviewUrl = null;
            backPreviewUrl = null;
        }
        const sellerUserId = Number(authenticatedUserId || dto.sellerUserId);
        if (!Number.isFinite(sellerUserId)) {
            throw new common_1.BadRequestException("sellerUserId is required.");
        }
        const validityDays = this.getDefaultValidityDays();
        const listing = this.listingRepository.create({
            sellerUserId,
            emirate: preview.emirate,
            emirateLabel: preview.emirateLabel,
            design: preview.design,
            designLabel: preview.designLabel,
            code: preview.code,
            number: preview.number,
            digitCount: this.deriveDigitCount(dto.number, dto.digitCount),
            vehicleType,
            plateType: this.nullable(dto.plateType),
            flag1: preview.flag1,
            flag2: preview.flag2,
            price: Number(dto.price),
            isNegotiable: Boolean(dto.isNegotiable),
            currency: dto.currency || "AED",
            description: dto.description || null,
            sellerName: dto.sellerName || null,
            sellerMobile: dto.sellerMobile || null,
            sellerEmail: dto.sellerEmail || null,
            plateImageUrl: preview.imageUrl,
            uploadedPlateImageUrl: this.nullable(dto.uploadedPlateImageUrl),
            frontPreviewUrl,
            backPreviewUrl,
            expiresAt: null,
            validityDays,
            isVisible: false,
            status: marketplace_listing_status_enum_1.MarketplaceListingStatus.PENDING_APPROVAL,
        });
        return this.listingRepository.save(listing);
    }
    async findMarketplaceListings(query) {
        await this.cleanupExpiredListings();
        const qb = this.listingRepository
            .createQueryBuilder("listing")
            .orderBy("listing.createdAt", "DESC");
        if (query.status) {
            qb.where("listing.status = :status", { status: query.status });
        }
        else {
            qb.where("listing.status IN (:...statuses)", { statuses: marketplace_listing_status_enum_1.PUBLIC_MARKETPLACE_STATUSES });
        }
        qb.andWhere("(listing.isVisible = :visible OR listing.status = :sold)", { visible: true, sold: marketplace_listing_status_enum_1.MarketplaceListingStatus.SOLD });
        qb.andWhere("(listing.expiresAt IS NULL OR listing.expiresAt > :now OR listing.status = :sold2)", { now: new Date(), sold2: marketplace_listing_status_enum_1.MarketplaceListingStatus.SOLD });
        if (query.emirate) {
            qb.andWhere("listing.emirate = :emirate", { emirate: this.normalizeKey(query.emirate) });
        }
        if (query.code) {
            qb.andWhere("listing.code = :code", { code: query.code.trim().toUpperCase() });
        }
        const digitCount = Number(query.digitCount);
        if (Number.isFinite(digitCount)) {
            qb.andWhere("listing.digitCount = :digitCount", { digitCount });
        }
        if (query.plateType) {
            qb.andWhere("LOWER(listing.plateType) = :plateType", {
                plateType: query.plateType.trim().toLowerCase(),
            });
        }
        const minPrice = Number(query.minPrice);
        if (Number.isFinite(minPrice)) {
            qb.andWhere("listing.price >= :minPrice", { minPrice });
        }
        const maxPrice = Number(query.maxPrice);
        if (Number.isFinite(maxPrice)) {
            qb.andWhere("listing.price <= :maxPrice", { maxPrice });
        }
        return qb.getMany();
    }
    async findListing(id, requesterUserId, allowPrivate = false) {
        await this.cleanupExpiredListings();
        const listing = await this.listingRepository.findOneBy({ id });
        if (!listing) {
            throw new common_1.NotFoundException(`Number plate listing with ID ${id} not found.`);
        }
        if (listing.status === marketplace_listing_status_enum_1.MarketplaceListingStatus.INACTIVE) {
            throw new common_1.NotFoundException(`Number plate listing with ID ${id} not found.`);
        }
        const isOwner = requesterUserId && Number(listing.sellerUserId) === Number(requesterUserId);
        const isPublic = marketplace_listing_status_enum_1.PUBLIC_MARKETPLACE_STATUSES.includes(listing.status) &&
            (listing.isVisible || listing.status === marketplace_listing_status_enum_1.MarketplaceListingStatus.SOLD) &&
            (!listing.expiresAt || listing.expiresAt > new Date() || listing.status === marketplace_listing_status_enum_1.MarketplaceListingStatus.SOLD);
        if (!isPublic && !isOwner && !allowPrivate) {
            throw new common_1.NotFoundException(`Number plate listing with ID ${id} not found.`);
        }
        return listing;
    }
    async findSellerListings(sellerUserId) {
        await this.cleanupExpiredListings();
        return this.listingRepository.find({
            where: { sellerUserId, parentListingId: null },
            order: { createdAt: "DESC" },
        });
    }
    async updateListing(listingId, dto, authenticatedUserId) {
        const listing = await this.findListing(listingId, authenticatedUserId, true);
        this.ensureListingOwnership(listing, authenticatedUserId);
        if ([
            marketplace_listing_status_enum_1.MarketplaceListingStatus.SOLD,
            marketplace_listing_status_enum_1.MarketplaceListingStatus.EXPIRED,
            marketplace_listing_status_enum_1.MarketplaceListingStatus.INACTIVE,
        ].includes(listing.status)) {
            throw new common_1.ConflictException("This plate listing can no longer be edited.");
        }
        if (!listing.approvedAt) {
            await this.applyListingUpdateFields(listing, dto);
            const saved = await this.listingRepository.save(listing);
            return Object.assign(saved, { moderationPending: false });
        }
        const existingEditRequest = await this.listingRepository.findOneBy({
            parentListingId: listing.id,
            status: marketplace_listing_status_enum_1.MarketplaceListingStatus.PENDING_APPROVAL,
        });
        const editRequest = existingEditRequest ||
            this.listingRepository.create({
                ...listing,
                id: undefined,
                parentListingId: listing.id,
                status: marketplace_listing_status_enum_1.MarketplaceListingStatus.PENDING_APPROVAL,
                isVisible: false,
                approvedAt: null,
                approvedByUserId: null,
                expiresAt: null,
                rejectionReason: null,
                createdAt: undefined,
                updatedAt: undefined,
            });
        await this.applyListingUpdateFields(editRequest, dto);
        const savedEditRequest = await this.listingRepository.save(editRequest);
        return Object.assign(listing, {
            moderationPending: true,
            pendingEditId: savedEditRequest?.id,
        });
    }
    async updateListingStatus(listingId, dto, authenticatedUserId) {
        const listing = await this.findListing(listingId, authenticatedUserId, true);
        this.ensureListingOwnership(listing, authenticatedUserId);
        if (!marketplace_listing_status_enum_1.SELLER_MUTABLE_MARKETPLACE_STATUSES.includes(listing.status)) {
            throw new common_1.ConflictException("This plate listing status can only be changed by admin.");
        }
        if (![
            marketplace_listing_status_enum_1.MarketplaceListingStatus.ACTIVE,
            marketplace_listing_status_enum_1.MarketplaceListingStatus.PAUSED,
            marketplace_listing_status_enum_1.MarketplaceListingStatus.SOLD,
        ].includes(dto.status)) {
            throw new common_1.BadRequestException("Users can only switch listings to active, paused, or sold.");
        }
        listing.status = dto.status;
        if (dto.status === marketplace_listing_status_enum_1.MarketplaceListingStatus.ACTIVE && !listing.approvedAt) {
            listing.status = marketplace_listing_status_enum_1.MarketplaceListingStatus.PENDING_APPROVAL;
        }
        if (dto.status === marketplace_listing_status_enum_1.MarketplaceListingStatus.SOLD) {
            listing.isVisible = false;
        }
        return this.listingRepository.save(listing);
    }
    async removeListing(listingId, authenticatedUserId) {
        const listing = await this.findListing(listingId, authenticatedUserId, true);
        this.ensureListingOwnership(listing, authenticatedUserId);
        listing.status = marketplace_listing_status_enum_1.MarketplaceListingStatus.INACTIVE;
        listing.isVisible = false;
        return this.listingRepository.save(listing);
    }
    async markSold(listingId, dto, authenticatedUserId) {
        const listing = await this.findListing(listingId, authenticatedUserId, true);
        const ownerId = Number(authenticatedUserId || dto.sellerUserId);
        if (Number(listing.sellerUserId) !== ownerId) {
            throw new common_1.ConflictException("Only the listing owner can mark this plate as sold.");
        }
        if (listing.status === marketplace_listing_status_enum_1.MarketplaceListingStatus.SOLD) {
            return listing;
        }
        listing.status = marketplace_listing_status_enum_1.MarketplaceListingStatus.SOLD;
        listing.isVisible = false;
        return this.listingRepository.save(listing);
    }
    async getListingPreview(listingId, requesterUserId) {
        const listing = await this.findListing(listingId, requesterUserId, true);
        return {
            id: listing.id,
            plateImageUrl: listing.plateImageUrl,
            frontPreviewUrl: listing.frontPreviewUrl,
            backPreviewUrl: listing.backPreviewUrl,
            emirate: listing.emirate,
            emirateLabel: listing.emirateLabel,
            code: listing.code,
            number: listing.number,
            plateType: listing.plateType,
        };
    }
    async adminFindListings(query) {
        await this.cleanupExpiredListings();
        const qb = this.listingRepository.createQueryBuilder("listing").orderBy("listing.createdAt", "DESC");
        if (query.status) {
            qb.where("listing.status = :status", { status: query.status });
        }
        else {
            qb.where("1 = 1");
        }
        if (query.emirate) {
            qb.andWhere("listing.emirate = :emirate", { emirate: this.normalizeKey(query.emirate) });
        }
        if (query.code) {
            qb.andWhere("listing.code = :code", { code: query.code.trim().toUpperCase() });
        }
        const digitCount = Number(query.digitCount);
        if (Number.isFinite(digitCount)) {
            qb.andWhere("listing.digitCount = :digitCount", { digitCount });
        }
        if (query.plateType) {
            qb.andWhere("LOWER(listing.plateType) = :plateType", {
                plateType: query.plateType.trim().toLowerCase(),
            });
        }
        const sellerUserId = Number(query.sellerUserId);
        if (Number.isFinite(sellerUserId)) {
            qb.andWhere("listing.sellerUserId = :sellerUserId", { sellerUserId });
        }
        const minPrice = Number(query.minPrice);
        if (Number.isFinite(minPrice)) {
            qb.andWhere("listing.price >= :minPrice", { minPrice });
        }
        const maxPrice = Number(query.maxPrice);
        if (Number.isFinite(maxPrice)) {
            qb.andWhere("listing.price <= :maxPrice", { maxPrice });
        }
        return qb.getMany();
    }
    async adminModerateListing(listingId, adminUserId, dto) {
        const listing = await this.findListing(listingId, adminUserId, true);
        if (listing.parentListingId) {
            const parentListing = await this.findListing(Number(listing.parentListingId), adminUserId, true);
            if (dto.status && dto.status === marketplace_listing_status_enum_1.MarketplaceListingStatus.REJECTED) {
                if (dto.rejectionReason !== undefined) {
                    listing.rejectionReason = this.nullable(dto.rejectionReason);
                }
                listing.status = marketplace_listing_status_enum_1.MarketplaceListingStatus.REJECTED;
                listing.isVisible = false;
                await this.listingRepository.save(listing);
                return parentListing;
            }
            if (dto.validityDays) {
                parentListing.validityDays = dto.validityDays;
                parentListing.expiresAt = this.calculateExpiryDate(dto.validityDays);
            }
            if (typeof dto.isVisible === "boolean") {
                parentListing.isVisible = dto.isVisible;
            }
            const shouldApproveEditRequest = Boolean(dto.status) && dto.status !== marketplace_listing_status_enum_1.MarketplaceListingStatus.PENDING_APPROVAL;
            if (shouldApproveEditRequest) {
                this.applyEditRequestFields(parentListing, listing);
                parentListing.rejectionReason = null;
                if (dto.status) {
                    parentListing.status = dto.status;
                }
                if (dto.status === marketplace_listing_status_enum_1.MarketplaceListingStatus.ACTIVE ||
                    dto.status === marketplace_listing_status_enum_1.MarketplaceListingStatus.AVAILABLE) {
                    parentListing.approvedAt = new Date();
                    parentListing.approvedByUserId = adminUserId || null;
                    parentListing.publishedAt = parentListing.publishedAt || new Date();
                    if (!parentListing.expiresAt) {
                        parentListing.expiresAt = this.calculateExpiryDate(parentListing.validityDays);
                    }
                    if (dto.isVisible === undefined) {
                        parentListing.isVisible = parentListing.isVisible ?? true;
                    }
                }
            }
            if (dto.status &&
                [
                    marketplace_listing_status_enum_1.MarketplaceListingStatus.REJECTED,
                    marketplace_listing_status_enum_1.MarketplaceListingStatus.INACTIVE,
                    marketplace_listing_status_enum_1.MarketplaceListingStatus.SOLD,
                    marketplace_listing_status_enum_1.MarketplaceListingStatus.EXPIRED,
                ].includes(dto.status)) {
                parentListing.isVisible = false;
            }
            const savedParent = await this.listingRepository.save(parentListing);
            if (shouldApproveEditRequest) {
                listing.status = marketplace_listing_status_enum_1.MarketplaceListingStatus.INACTIVE;
                listing.isVisible = false;
                await this.listingRepository.save(listing);
            }
            return savedParent;
        }
        if (dto.validityDays) {
            listing.validityDays = dto.validityDays;
            listing.expiresAt = this.calculateExpiryDate(dto.validityDays);
        }
        if (typeof dto.isVisible === "boolean") {
            listing.isVisible = dto.isVisible;
        }
        if (dto.rejectionReason !== undefined) {
            listing.rejectionReason = this.nullable(dto.rejectionReason);
        }
        if (dto.status) {
            listing.status = dto.status;
            if (dto.status === marketplace_listing_status_enum_1.MarketplaceListingStatus.ACTIVE ||
                dto.status === marketplace_listing_status_enum_1.MarketplaceListingStatus.AVAILABLE) {
                listing.approvedAt = new Date();
                listing.approvedByUserId = adminUserId || null;
                listing.publishedAt = listing.publishedAt || new Date();
                listing.isVisible = dto.isVisible ?? true;
                listing.expiresAt = this.calculateExpiryDate(listing.validityDays);
            }
            if ([
                marketplace_listing_status_enum_1.MarketplaceListingStatus.REJECTED,
                marketplace_listing_status_enum_1.MarketplaceListingStatus.INACTIVE,
                marketplace_listing_status_enum_1.MarketplaceListingStatus.SOLD,
                marketplace_listing_status_enum_1.MarketplaceListingStatus.EXPIRED,
            ].includes(dto.status)) {
                listing.isVisible = false;
            }
        }
        return this.listingRepository.save(listing);
    }
    async applyListingUpdateFields(listing, dto) {
        const vehicleType = dto.vehicleType || listing.vehicleType || "car";
        const previewInput = {
            vehicleType,
            emirate: dto.emirate || listing.emirate,
            design: dto.design || listing.design,
            code: dto.code || listing.code,
            number: dto.number || listing.number,
            flag1: dto.flag1 || listing.flag1 || "0",
            flag2: dto.flag2 || listing.flag2 || "0",
            width: "1200",
        };
        const preview = this.generatePreview(previewInput);
        let frontPreviewUrl = listing.frontPreviewUrl;
        let backPreviewUrl = listing.backPreviewUrl;
        try {
            const carSlug = dto.carSlug || (vehicleType === "bike" ? "kawasaki" : DEFAULT_CAR_SLUG);
            const carPreview = await this.generateCarPreview({
                plateImageUrl: preview.imageUrl,
                carSlug,
                publicBaseUrl: dto.publicBaseUrl,
            });
            frontPreviewUrl = carPreview.front?.previewUrl || listing.frontPreviewUrl;
            backPreviewUrl = carPreview.back?.previewUrl || listing.backPreviewUrl;
        }
        catch (error) {
            frontPreviewUrl = listing.frontPreviewUrl;
            backPreviewUrl = listing.backPreviewUrl;
        }
        Object.assign(listing, {
            emirate: preview.emirate,
            emirateLabel: preview.emirateLabel,
            design: preview.design,
            designLabel: preview.designLabel,
            code: preview.code,
            number: preview.number,
            digitCount: this.deriveDigitCount(preview.number, dto.digitCount ?? listing.digitCount),
            vehicleType,
            plateType: this.preserveNullable(dto.plateType, listing.plateType),
            flag1: preview.flag1,
            flag2: preview.flag2,
            price: dto.price ?? listing.price,
            isNegotiable: dto.isNegotiable ?? listing.isNegotiable,
            currency: dto.currency ?? listing.currency,
            description: this.preserveNullable(dto.description, listing.description),
            sellerName: this.preserveNullable(dto.sellerName, listing.sellerName),
            sellerMobile: this.preserveNullable(dto.sellerMobile, listing.sellerMobile),
            sellerEmail: this.preserveNullable(dto.sellerEmail, listing.sellerEmail),
            plateImageUrl: preview.imageUrl,
            uploadedPlateImageUrl: this.preserveNullable(dto.uploadedPlateImageUrl, listing.uploadedPlateImageUrl),
            frontPreviewUrl,
            backPreviewUrl,
        });
    }
    applyEditRequestFields(parent, editRequest) {
        Object.assign(parent, {
            emirate: editRequest.emirate,
            emirateLabel: editRequest.emirateLabel,
            design: editRequest.design,
            designLabel: editRequest.designLabel,
            code: editRequest.code,
            number: editRequest.number,
            digitCount: editRequest.digitCount,
            vehicleType: editRequest.vehicleType,
            plateType: editRequest.plateType,
            flag1: editRequest.flag1,
            flag2: editRequest.flag2,
            price: editRequest.price,
            isNegotiable: editRequest.isNegotiable,
            currency: editRequest.currency,
            description: editRequest.description,
            sellerName: editRequest.sellerName,
            sellerMobile: editRequest.sellerMobile,
            sellerEmail: editRequest.sellerEmail,
            plateImageUrl: editRequest.plateImageUrl,
            uploadedPlateImageUrl: editRequest.uploadedPlateImageUrl,
            frontPreviewUrl: editRequest.frontPreviewUrl,
            backPreviewUrl: editRequest.backPreviewUrl,
        });
    }
    generatePreview(query) {
        const vehicleType = query.vehicleType?.trim().toLowerCase() || "car";
        return vehicleType === "bike"
            ? this.generateBikePreview(query)
            : this.generateCarPlatePreview(query);
    }
    generateCarPlatePreview(query) {
        const emirateKey = this.normalizeKey(query.emirate);
        const emirateConfig = plateConfig[emirateKey];
        if (!emirateConfig) {
            throw new common_1.BadRequestException("Invalid emirate.");
        }
        const designKey = this.normalizeKey(query.design || "normal");
        const designConfig = emirateConfig.designs[designKey];
        if (!designConfig) {
            throw new common_1.BadRequestException("Invalid design for selected emirate.");
        }
        const code = (query.code || "").trim().toUpperCase();
        const number = query.number.trim();
        const flag1 = query.flag1 || "0";
        const flag2 = query.flag2 || "0";
        const width = this.normalizeWidth(query.width);
        if (!code) {
            throw new common_1.BadRequestException("Plate code is required.");
        }
        if (!number) {
            throw new common_1.BadRequestException("Plate number is required.");
        }
        const imageUrl = this.buildImageUrl(designConfig.slug, code, number, flag1, flag2, width);
        return {
            vehicleType: "car",
            imageUrl,
            emirate: emirateKey,
            emirateLabel: emirateConfig.label,
            design: designKey,
            designLabel: designConfig.label,
            slug: designConfig.slug,
            code,
            number,
            flag1,
            flag2,
            width,
            isKnownCode: designConfig.codes.includes(code),
            availableCodes: designConfig.codes,
        };
    }
    generateBikePreview(query) {
        const emirateKey = this.normalizeKey(query.emirate);
        const emirateConfig = bikePlateConfig[emirateKey];
        if (!emirateConfig) {
            throw new common_1.BadRequestException(`Invalid emirate for bike plates. Available: ${Object.keys(bikePlateConfig).join(", ")}`);
        }
        const designKey = this.normalizeKey(query.design || "normal");
        const designConfig = emirateConfig.designs[designKey];
        if (!designConfig) {
            throw new common_1.BadRequestException("Invalid design for selected emirate.");
        }
        const code = (query.code || "").trim();
        const number = query.number.trim();
        const width = this.normalizeWidth(query.width || "300");
        if (!code) {
            throw new common_1.BadRequestException("Plate code is required for bike plates.");
        }
        if (!number) {
            throw new common_1.BadRequestException("Plate number is required.");
        }
        const imageUrl = this.buildBikeImageUrl(designConfig.slug, width, code, number);
        return {
            vehicleType: "bike",
            imageUrl,
            emirate: emirateKey,
            emirateLabel: emirateConfig.label,
            design: designKey,
            designLabel: designConfig.label,
            slug: designConfig.slug,
            code,
            number,
            flag1: code,
            flag2: "0",
            width,
        };
    }
    async generateCarPreview(query) {
        let plateImageUrl = query.plateImageUrl?.trim();
        if (!plateImageUrl) {
            if (!query.emirate || !query.code || !query.number) {
                throw new common_1.BadRequestException("Provide either plateImageUrl or all of: emirate, code, number.");
            }
            const preview = this.generatePreview({
                vehicleType: query.vehicleType || "car",
                emirate: query.emirate,
                design: query.design,
                code: query.code,
                number: query.number,
                width: query.width,
            });
            plateImageUrl = preview.imageUrl;
        }
        const slug = query.carSlug?.trim() || DEFAULT_CAR_SLUG;
        const carAsset = carAssets[slug];
        if (!carAsset) {
            throw new common_1.BadRequestException(`Unknown car slug: "${slug}". Available: ${Object.keys(carAssets).join(", ")}`);
        }
        const expandRatio = this.normalizeExpandRatio(query.expandRatio);
        const publicBaseUrl = query.publicBaseUrl?.trim().replace(/\/$/, "");
        const drawBox = this.normalizeBoolean(query.drawBox);
        const offsetY = this.parseOffset(query.offsetY, 0);
        const results = {};
        results.front = carAsset.front
            ? await this.generateSingleCarPreview("front", plateImageUrl, expandRatio, carAsset.front, publicBaseUrl, drawBox, offsetY)
            : null;
        results.back = await this.generateSingleCarPreview("back", plateImageUrl, expandRatio, carAsset.back, publicBaseUrl, drawBox, offsetY);
        return {
            plateImageUrl,
            carSlug: slug,
            carLabel: carAsset.label,
            expandRatio,
            ...results,
        };
    }
    async generateSingleCarPreview(side, plateImageUrl, expandRatio, defaultAsset, publicBaseUrl, drawBox = false, offsetY = 0) {
        const resolvedPlateImageUrl = this.resolvePublicUrl(plateImageUrl, publicBaseUrl);
        const plateImageForSvg = await this.resolveImageForSvg(resolvedPlateImageUrl);
        const resolvedCarImageUrl = await this.fileToDataUri(defaultAsset.path, this.getMimeType(defaultAsset.path));
        const detectionResult = {
            source: "default_asset",
            image: { width: defaultAsset.width, height: defaultAsset.height },
            predictions: [defaultAsset.detection],
        };
        const dimensions = { width: defaultAsset.width, height: defaultAsset.height };
        const bestDetection = defaultAsset.detection;
        const box = this.expandDetectionBox(bestDetection, dimensions.width, dimensions.height, expandRatio, offsetY);
        const previewPath = await this.writePreviewSvg(side, resolvedCarImageUrl, plateImageForSvg, dimensions.width, dimensions.height, box, drawBox);
        return {
            carImageUrl: null,
            defaultAsset: defaultAsset.path,
            previewUrl: publicBaseUrl ? `${publicBaseUrl}${previewPath}` : previewPath,
            previewPath,
            detection: bestDetection,
            appliedBox: box,
            image: dimensions,
            rawDetectionResult: detectionResult,
        };
    }
    expandDetectionBox(detection, imageWidth, imageHeight, expandRatio, offsetY = 0) {
        const boxWidth = Math.round(detection.width * expandRatio);
        const boxHeight = Math.round(detection.height * expandRatio);
        const x = Math.round(detection.x - boxWidth / 2);
        const y = Math.round(detection.y - boxHeight / 2 + offsetY);
        const clampedX = Math.max(0, x);
        const clampedY = Math.max(0, y);
        const clampedWidth = Math.min(boxWidth, imageWidth - clampedX);
        const clampedHeight = Math.min(boxHeight, imageHeight - clampedY);
        return {
            x: clampedX,
            y: clampedY,
            width: clampedWidth,
            height: clampedHeight,
        };
    }
    async writePreviewSvg(side, carImageUrl, plateImageUrl, width, height, box, drawBox) {
        const uploadDir = (0, path_1.join)(process.cwd(), "uploads", "number-plate-previews");
        await fs_1.promises.mkdir(uploadDir, { recursive: true });
        const fileName = `${Date.now()}-${side}-${Math.round(Math.random() * 1e9)}.svg`;
        const filePath = (0, path_1.join)(uploadDir, fileName);
        const svg = [
            `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`,
            `<image href="${this.escapeXml(carImageUrl)}" x="0" y="0" width="${width}" height="${height}" preserveAspectRatio="xMidYMid meet"/>`,
            `<image href="${this.escapeXml(plateImageUrl)}" x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" preserveAspectRatio="none"/>`,
            drawBox
                ? `<rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" fill="none" stroke="red" stroke-width="5"/>`
                : "",
            "</svg>",
        ].join("");
        await fs_1.promises.writeFile(filePath, svg, "utf8");
        return `/uploads/number-plate-previews/${fileName}`;
    }
    buildImageUrl(slug, code, number, flag1, flag2, width) {
        const encodedCode = encodeURIComponent(code);
        const encodedNumber = encodeURIComponent(number);
        return `https://xplate.io/en/numbers/images/license-plate/${slug}/${encodedCode}/${encodedNumber}/${flag1}/${flag2}/${width}`;
    }
    buildBikeImageUrl(slug, width, code, number) {
        return `https://cdn.emiratesauction.com/PLATES/${slug}/${width}/${code}/${encodeURIComponent(number)}.png`;
    }
    normalizeKey(value) {
        return value.trim().toLowerCase();
    }
    normalizeWidth(width) {
        const parsedWidth = Number(width || "1200");
        if (!Number.isFinite(parsedWidth) || parsedWidth < 300) {
            return "1200";
        }
        return String(Math.round(parsedWidth));
    }
    normalizeExpandRatio(expandRatio) {
        const parsed = Number(expandRatio || "1.01");
        if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 2) {
            return 1.01;
        }
        return parsed;
    }
    normalizeBoolean(value) {
        return ["1", "true", "yes"].includes(String(value || "").trim().toLowerCase());
    }
    getDefaultValidityDays() {
        const rawDays = this.configService.get("NUMBER_PLATE_LISTING_EXPIRE_DAYS") || "30";
        const days = Number(rawDays);
        return Number.isFinite(days) && days > 0 ? days : 30;
    }
    calculateExpiryDate(validityDays) {
        const days = validityDays || this.getDefaultValidityDays();
        const safeDays = Number.isFinite(days) && days > 0 ? days : 30;
        const expiresAt = new Date();
        expiresAt.setDate(expiresAt.getDate() + safeDays);
        return expiresAt;
    }
    startExpiryCleanupLoop() {
        this.expiryCleanupTimer = setInterval(() => {
            void this.cleanupExpiredListings();
        }, 60 * 60 * 1000);
    }
    async cleanupExpiredListings() {
        await this.listingRepository
            .createQueryBuilder()
            .update(numberplate_listing_entity_1.NumberPlateListing)
            .set({
            status: marketplace_listing_status_enum_1.MarketplaceListingStatus.EXPIRED,
            isVisible: false,
        })
            .where("expiresAt IS NOT NULL")
            .andWhere("expiresAt <= :now", { now: new Date() })
            .andWhere("status IN (:...statuses)", {
            statuses: [
                marketplace_listing_status_enum_1.MarketplaceListingStatus.ACTIVE,
                marketplace_listing_status_enum_1.MarketplaceListingStatus.AVAILABLE,
                marketplace_listing_status_enum_1.MarketplaceListingStatus.PAUSED,
            ],
        })
            .execute();
    }
    buildWhatsappUrl(mobile, listing) {
        const normalizedMobile = this.normalizePhoneNumber(mobile);
        if (!normalizedMobile) {
            return null;
        }
        const message = this.buildWhatsappText(listing);
        return `https://wa.me/${normalizedMobile}?text=${encodeURIComponent(message)}`;
    }
    buildWhatsappText(listing) {
        return [
            "Hello, I am interested in your license plate listing.",
            `Plate: ${listing.emirateLabel || listing.emirate} ${listing.code} ${listing.number}`,
            `Price: ${listing.currency || "AED"} ${listing.price}`,
        ].join(" ");
    }
    ensureListingOwnership(listing, authenticatedUserId) {
        if (Number(listing.sellerUserId) !== Number(authenticatedUserId)) {
            throw new common_1.ConflictException("Only the listing owner can manage this plate listing.");
        }
    }
    deriveDigitCount(numberValue, fallback) {
        const digits = String(numberValue || "").replace(/[^\d]/g, "");
        if (digits.length > 0) {
            return digits.length;
        }
        const candidate = Number(fallback);
        return Number.isFinite(candidate) && candidate > 0 ? candidate : null;
    }
    normalizePhoneNumber(value) {
        const digits = String(value || "").replace(/[^\d]/g, "");
        if (!digits) {
            return null;
        }
        return digits.startsWith("971") ? digits : `971${digits.replace(/^0+/, "")}`;
    }
    parseOffset(value, defaultValue) {
        const parsed = Number(value);
        return Number.isFinite(parsed) ? parsed : defaultValue;
    }
    resolvePublicUrl(url, publicBaseUrl) {
        const trimmedUrl = url.trim();
        if (/^https?:\/\//i.test(trimmedUrl)) {
            return trimmedUrl;
        }
        if (publicBaseUrl && trimmedUrl.startsWith("/")) {
            return `${publicBaseUrl}${trimmedUrl}`;
        }
        return trimmedUrl;
    }
    async resolveImageForSvg(imageUrl) {
        if (imageUrl.startsWith("data:")) {
            return imageUrl;
        }
        try {
            if (imageUrl.startsWith("/uploads/")) {
                const filePath = (0, path_1.join)(process.cwd(), imageUrl.replace(/^\//, ""));
                const mimeType = this.getMimeType(filePath);
                return this.fileToDataUri(filePath, mimeType);
            }
            if (/^https?:\/\//i.test(imageUrl)) {
                const response = await axios_1.default.get(imageUrl, {
                    responseType: "arraybuffer",
                    timeout: 15000,
                });
                const contentType = response.headers["content-type"] || this.getMimeType(imageUrl);
                const base64 = Buffer.from(response.data).toString("base64");
                return `data:${contentType};base64,${base64}`;
            }
        }
        catch (error) {
            return imageUrl;
        }
        return imageUrl;
    }
    getMimeType(pathOrUrl) {
        const value = pathOrUrl.toLowerCase().split("?")[0];
        if (value.endsWith(".jpg") || value.endsWith(".jpeg")) {
            return "image/jpeg";
        }
        if (value.endsWith(".webp")) {
            return "image/webp";
        }
        if (value.endsWith(".svg")) {
            return "image/svg+xml";
        }
        return "image/png";
    }
    escapeXml(value) {
        return value
            .replace(/&/g, "&amp;")
            .replace(/"/g, "&quot;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;");
    }
    async fileToDataUri(filePath, mimeType) {
        const data = await fs_1.promises.readFile(filePath);
        return `data:${mimeType};base64,${data.toString("base64")}`;
    }
    nullable(value) {
        const normalized = String(value || "").trim();
        return normalized || null;
    }
    preserveNullable(nextValue, currentValue) {
        if (nextValue === undefined) {
            return currentValue;
        }
        return this.nullable(nextValue);
    }
};
exports.NumberPlateService = NumberPlateService;
exports.NumberPlateService = NumberPlateService = __decorate([
    (0, common_1.Injectable)(),
    __param(1, (0, typeorm_1.InjectRepository)(numberplate_listing_entity_1.NumberPlateListing)),
    __metadata("design:paramtypes", [config_1.ConfigService,
        typeorm_2.Repository])
], NumberPlateService);
//# sourceMappingURL=numberplate.service.js.map