import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { and, count, eq, gte, ilike, inArray, lte, ne, or, SQL, sql } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston';
import { ZodError } from 'zod';
import * as schema from '../drizzle/schema.js';
import { commercials, PG_CONNECTION } from '../drizzle/schema.js';
import { FileService } from '../file/file.service.js';
import { GeoService } from '../geo/geo.service.js';
import { CommercialPropertyListing, CommercialPropertyListingSchema } from '../models/db.model.js';
import { CommercialPropertyListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js';
import { convertCommercialToDrizzleCommercial, convertDrizzleCommercialToCommercial, getDistanceQuery } from '../utils.js';

@Injectable()
export class CommercialPropertyService {
  constructor(
    @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
    @Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
    private fileService?: FileService,
    private geoService?: GeoService,
  ) {}
  private getWhereConditions(criteria: CommercialPropertyListingCriteria): SQL[] {
    const whereConditions: SQL[] = [];

    if (criteria.city && criteria.searchType === 'exact') {
      whereConditions.push(ilike(schema.commercials.city, `%${criteria.city}%`));
    }
    if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
      const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city);
      whereConditions.push(sql`${getDistanceQuery(commercials, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
    }
    if (criteria.types && criteria.types.length > 0) {
      whereConditions.push(inArray(schema.commercials.type, criteria.types));
    }

    if (criteria.state) {
      whereConditions.push(eq(schema.commercials.state, criteria.state));
    }

    if (criteria.minPrice) {
      whereConditions.push(gte(schema.commercials.price, criteria.minPrice));
    }

    if (criteria.maxPrice) {
      whereConditions.push(lte(schema.commercials.price, criteria.maxPrice));
    }

    if (criteria.title) {
      whereConditions.push(or(ilike(schema.commercials.title, `%${criteria.title}%`), ilike(schema.commercials.description, `%${criteria.title}%`)));
    }
    whereConditions.push(and(eq(schema.users.customerType, 'professional')));
    return whereConditions;
  }
  // #### Find by criteria ########################################
  async searchCommercialProperties(criteria: CommercialPropertyListingCriteria, user: JwtUser): Promise<any> {
    const start = criteria.start ? criteria.start : 0;
    const length = criteria.length ? criteria.length : 12;
    const query = this.conn.select({ commercial: commercials }).from(commercials).leftJoin(schema.users, eq(commercials.email, schema.users.email));
    const whereConditions = this.getWhereConditions(criteria);

    if (whereConditions.length > 0) {
      const whereClause = and(...whereConditions);
      query.where(whereClause);
    }

    // Paginierung
    query.limit(length).offset(start);

    const data = await query;
    const results = data.map(r => r.commercial).map(r => convertDrizzleCommercialToCommercial(r));
    const totalCount = await this.getCommercialPropertiesCount(criteria);

    return {
      results,
      totalCount,
    };
  }
  async getCommercialPropertiesCount(criteria: CommercialPropertyListingCriteria): Promise<number> {
    const countQuery = this.conn.select({ value: count() }).from(schema.commercials).leftJoin(schema.users, eq(commercials.email, schema.users.email));
    const whereConditions = this.getWhereConditions(criteria);

    if (whereConditions.length > 0) {
      const whereClause = and(...whereConditions);
      countQuery.where(whereClause);
    }

    const [{ value: totalCount }] = await countQuery;
    return totalCount;
  }

  // #### Find by ID ########################################
  async findCommercialPropertiesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
    let result = await this.conn
      .select()
      .from(commercials)
      .where(and(sql`${commercials.id} = ${id}`));
    result = result.filter(r => !r.draft || r.imagePath === emailToDirName(user?.username) || user?.roles.includes('ADMIN'));
    return convertDrizzleCommercialToCommercial(result[0]) as CommercialPropertyListing;
  }

  // #### Find by User EMail ########################################
  async findCommercialPropertiesByEmail(email: string, user: JwtUser): Promise<CommercialPropertyListing[]> {
    const conditions = [];
    conditions.push(eq(commercials.imagePath, emailToDirName(email)));
    if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) {
      conditions.push(ne(commercials.draft, true));
    }
    const listings = (await this.conn
      .select()
      .from(commercials)
      .where(and(...conditions))) as CommercialPropertyListing[];
    return listings.map(l => convertDrizzleCommercialToCommercial(l)) as CommercialPropertyListing[];
  }
  // #### Find by imagePath ########################################
  async findByImagePath(imagePath: string, serial: string): Promise<CommercialPropertyListing> {
    const result = await this.conn
      .select()
      .from(commercials)
      .where(and(sql`${commercials.imagePath} = ${imagePath}`, sql`${commercials.serialId} = ${serial}`));
    return convertDrizzleCommercialToCommercial(result[0]) as CommercialPropertyListing;
  }
  // #### CREATE ########################################
  async createListing(data: CommercialPropertyListing): Promise<CommercialPropertyListing> {
    try {
      data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
      data.updated = new Date();
      const validatedCommercialPropertyListing = CommercialPropertyListingSchema.parse(data);
      const convertedCommercialPropertyListing = convertCommercialToDrizzleCommercial(data);
      const [createdListing] = await this.conn.insert(commercials).values(convertedCommercialPropertyListing).returning();
      return convertDrizzleCommercialToCommercial(createdListing);
    } catch (error) {
      if (error instanceof ZodError) {
        const formattedErrors = error.errors.map(err => ({
          field: err.path.join('.'),
          message: err.message,
        }));
        throw new BadRequestException(formattedErrors);
      }
      throw error;
    }
  }
  // #### UPDATE CommercialProps ########################################
  async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing): Promise<CommercialPropertyListing> {
    try {
      data.updated = new Date();
      data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
      const validatedCommercialPropertyListing = CommercialPropertyListingSchema.parse(data);
      const imageOrder = await this.fileService.getPropertyImages(data.imagePath, String(data.serialId));
      let difference = imageOrder.filter(x => !data.imageOrder.includes(x)).concat(data.imageOrder.filter(x => !imageOrder.includes(x)));
      if (difference.length > 0) {
        this.logger.warn(`changes between image directory and imageOrder in listing ${data.serialId}: ${difference.join(',')}`);
        data.imageOrder = imageOrder;
      }
      const convertedCommercialPropertyListing = convertCommercialToDrizzleCommercial(data);
      const [updateListing] = await this.conn.update(commercials).set(convertedCommercialPropertyListing).where(eq(commercials.id, id)).returning();
      return convertDrizzleCommercialToCommercial(updateListing);
    } catch (error) {
      if (error instanceof ZodError) {
        const formattedErrors = error.errors.map(err => ({
          field: err.path.join('.'),
          message: err.message,
        }));
        throw new BadRequestException(formattedErrors);
      }
      throw error;
    }
  }
  // ##############################################################
  // Images for commercial Properties
  // ##############################################################
  async deleteImage(imagePath: string, serial: string, name: string) {
    const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing;
    const index = listing.imageOrder.findIndex(im => im === name);
    if (index > -1) {
      listing.imageOrder.splice(index, 1);
      await this.updateCommercialPropertyListing(listing.id, listing);
    }
  }
  async addImage(imagePath: string, serial: string, imagename: string) {
    const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing;
    listing.imageOrder.push(imagename);
    await this.updateCommercialPropertyListing(listing.id, listing);
  }
  // #### DELETE  ########################################
  async deleteListing(id: string): Promise<void> {
    await this.conn.delete(commercials).where(eq(commercials.id, id));
  }
  // ##############################################################
  // States
  // ##############################################################
  async getStates(): Promise<any[]> {
    return await this.conn
      .select({ state: commercials.state, count: sql<number>`count(${commercials.id})`.mapWith(Number) })
      .from(commercials)
      .groupBy(sql`${commercials.state}`)
      .orderBy(sql`count desc`);
  }
}
