import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { and, arrayContains, asc, count, desc, eq, gte, inArray, lte, 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';
import { commercials_json, PG_CONNECTION } from '../drizzle/schema';
import { FileService } from '../file/file.service';
import { GeoService } from '../geo/geo.service';
import { CommercialPropertyListing, CommercialPropertyListingSchema } from '../models/db.model';
import { CommercialPropertyListingCriteria, JwtUser } from '../models/main.model';
import { getDistanceQuery, splitName } from '../utils';
import { generateSlug, extractShortIdFromSlug, isSlug } from '../utils/slug.utils';

@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, user: JwtUser): SQL[] {
    const whereConditions: SQL[] = [];

    if (criteria.city && criteria.searchType === 'exact') {
      whereConditions.push(sql`(${commercials_json.data}->'location'->>'name') ILIKE ${criteria.city.name}`);
    }
    if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
      const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city.name);
      whereConditions.push(sql`${getDistanceQuery(commercials_json, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
    }
    if (criteria.types && criteria.types.length > 0) {
      this.logger.warn('Adding commercial property type filter', { types: criteria.types });
      // Use explicit SQL with IN for robust JSONB comparison
      const typeValues = criteria.types.map(t => sql`${t}`);
      whereConditions.push(sql`((${commercials_json.data}->>'type') IN (${sql.join(typeValues, sql`, `)}))`);
    }

    if (criteria.state) {
      whereConditions.push(sql`(${commercials_json.data}->'location'->>'state') = ${criteria.state}`);
    }

    if (criteria.minPrice) {
      whereConditions.push(gte(sql`(${commercials_json.data}->>'price')::double precision`, criteria.minPrice));
    }

    if (criteria.maxPrice) {
      whereConditions.push(lte(sql`(${commercials_json.data}->>'price')::double precision`, criteria.maxPrice));
    }

    if (criteria.title) {
      whereConditions.push(
        sql`((${commercials_json.data}->>'title') ILIKE ${`%${criteria.title}%`} OR (${commercials_json.data}->>'description') ILIKE ${`%${criteria.title}%`})`
      );
    }

    if (criteria.brokerName) {
      const { firstname, lastname } = splitName(criteria.brokerName);
      if (firstname === lastname) {
        // Single word: search either first OR last name
        whereConditions.push(
          sql`((${schema.users_json.data}->>'firstname') ILIKE ${`%${firstname}%`} OR (${schema.users_json.data}->>'lastname') ILIKE ${`%${lastname}%`})`
        );
      } else {
        // Multiple words: search both first AND last name
        whereConditions.push(
          sql`((${schema.users_json.data}->>'firstname') ILIKE ${`%${firstname}%`} AND (${schema.users_json.data}->>'lastname') ILIKE ${`%${lastname}%`})`
        );
      }
    }

    if (user?.role !== 'admin') {
      whereConditions.push(
        sql`((${commercials_json.email} = ${user?.email || null}) OR (${commercials_json.data}->>'draft')::boolean IS NOT TRUE)`
      );
    }
    this.logger.warn('whereConditions count', { count: whereConditions.length });
    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_json }).from(commercials_json).leftJoin(schema.users_json, eq(commercials_json.email, schema.users_json.email));
    const whereConditions = this.getWhereConditions(criteria, user);

    this.logger.warn('Filter Criteria:', { criteria: JSON.stringify(criteria) });

    if (whereConditions.length > 0) {
      const whereClause = sql.join(whereConditions, sql` AND `);
      query.where(sql`(${whereClause})`);

      this.logger.warn('Generated SQL:', { sql: query.toSQL().sql, params: query.toSQL().params });
    }
    // Sortierung
    switch (criteria.sortBy) {
      case 'priceAsc':
        query.orderBy(asc(sql`(${commercials_json.data}->>'price')::double precision`));
        break;
      case 'priceDesc':
        query.orderBy(desc(sql`(${commercials_json.data}->>'price')::double precision`));
        break;
      case 'creationDateFirst':
        query.orderBy(asc(sql`${commercials_json.data}->>'created'`));
        break;
      case 'creationDateLast':
        query.orderBy(desc(sql`${commercials_json.data}->>'created'`));
        break;
      default:
        // Keine spezifische Sortierung, Standardverhalten kann hier eingefügt werden
        break;
    }

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

    const data = await query;
    const results = data.map(r => ({ id: r.commercial.id, email: r.commercial.email, ...(r.commercial.data as CommercialPropertyListing) }));
    const totalCount = await this.getCommercialPropertiesCount(criteria, user);

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

    if (whereConditions.length > 0) {
      const whereClause = sql.join(whereConditions, sql` AND `);
      countQuery.where(sql`(${whereClause})`);
    }

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

  // #### Find by ID ########################################
  /**
   * Find commercial property by slug or ID
   * Supports both slug (e.g., "office-space-austin-tx-a3f7b2c1") and UUID
   */
  async findCommercialBySlugOrId(slugOrId: string, user: JwtUser): Promise<CommercialPropertyListing> {
    this.logger.debug(`findCommercialBySlugOrId called with: ${slugOrId}`);

    let id = slugOrId;

    // Check if it's a slug (contains multiple hyphens) vs UUID
    if (isSlug(slugOrId)) {
      this.logger.debug(`Detected as slug: ${slugOrId}`);

      // Extract short ID from slug and find by slug field
      const listing = await this.findCommercialBySlug(slugOrId);
      if (listing) {
        this.logger.debug(`Found listing by slug: ${slugOrId} -> ID: ${listing.id}`);
        id = listing.id;
      } else {
        this.logger.warn(`Slug not found in database: ${slugOrId}`);
        throw new NotFoundException(
          `Commercial property listing not found with slug: ${slugOrId}. ` +
          `The listing may have been deleted or the URL may be incorrect.`
        );
      }
    } else {
      this.logger.debug(`Detected as UUID: ${slugOrId}`);
    }

    return this.findCommercialPropertiesById(id, user);
  }

  /**
   * Find commercial property by slug
   */
  async findCommercialBySlug(slug: string): Promise<CommercialPropertyListing | null> {
    const result = await this.conn
      .select()
      .from(commercials_json)
      .where(sql`${commercials_json.data}->>'slug' = ${slug}`)
      .limit(1);

    if (result.length > 0) {
      return { id: result[0].id, email: result[0].email, ...(result[0].data as CommercialPropertyListing) } as CommercialPropertyListing;
    }
    return null;
  }

  async findCommercialPropertiesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
    const conditions = [];
    if (user?.role !== 'admin') {
      conditions.push(or(eq(commercials_json.email, user?.email), sql`(${commercials_json.data}->>'draft')::boolean IS NOT TRUE`));
    }
    conditions.push(eq(commercials_json.id, id));
    const result = await this.conn
      .select()
      .from(commercials_json)
      .where(and(...conditions));
    if (result.length > 0) {
      return { id: result[0].id, email: result[0].email, ...(result[0].data as CommercialPropertyListing) } as CommercialPropertyListing;
    } else {
      throw new BadRequestException(`No entry available for ${id}`);
    }
  }

  // #### Find by User EMail ########################################
  async findCommercialPropertiesByEmail(email: string, user: JwtUser): Promise<CommercialPropertyListing[]> {
    const conditions = [];
    conditions.push(eq(commercials_json.email, email));
    if (email !== user?.email && user?.role !== 'admin') {
      conditions.push(sql`(${commercials_json.data}->>'draft')::boolean IS NOT TRUE`);
    }
    const listings = await this.conn
      .select()
      .from(commercials_json)
      .where(and(...conditions));
    return listings.map(l => ({ id: l.id, email: l.email, ...(l.data as CommercialPropertyListing) }) as CommercialPropertyListing);
  }
  // #### Find Favorites ########################################
  async findFavoriteListings(user: JwtUser): Promise<CommercialPropertyListing[]> {
    const userFavorites = await this.conn
      .select()
      .from(commercials_json)
      .where(sql`${commercials_json.data}->'favoritesForUser' ? ${user.email}`);
    return userFavorites.map(l => ({ id: l.id, email: l.email, ...(l.data as CommercialPropertyListing) }) as CommercialPropertyListing);
  }
  // #### Find by imagePath ########################################
  async findByImagePath(imagePath: string, serial: string): Promise<CommercialPropertyListing> {
    const result = await this.conn
      .select()
      .from(commercials_json)
      .where(and(sql`(${commercials_json.data}->>'imagePath') = ${imagePath}`, sql`(${commercials_json.data}->>'serialId')::integer = ${serial}`));
    if (result.length > 0) {
      return { id: result[0].id, email: result[0].email, ...(result[0].data as CommercialPropertyListing) } as CommercialPropertyListing;
    }
  }
  // #### CREATE ########################################
  async createListing(data: CommercialPropertyListing): Promise<CommercialPropertyListing> {
    try {
      // Generate serialId based on timestamp + random number (temporary solution until sequence is created)
      // This ensures uniqueness without requiring a database sequence
      const serialId = Date.now() % 1000000 + Math.floor(Math.random() * 1000);

      data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
      data.updated = new Date();
      data.serialId = Number(serialId);
      CommercialPropertyListingSchema.parse(data);
      const { id, email, ...rest } = data;
      const convertedCommercialPropertyListing = { email, data: rest };
      const [createdListing] = await this.conn.insert(commercials_json).values(convertedCommercialPropertyListing).returning();

      // Generate and update slug after creation (we need the ID first)
      const slug = generateSlug(data.title, data.location, createdListing.id);
      const listingWithSlug = { ...(createdListing.data as any), slug };
      await this.conn.update(commercials_json).set({ data: listingWithSlug }).where(eq(commercials_json.id, createdListing.id));

      return { id: createdListing.id, email: createdListing.email, ...(createdListing.data as CommercialPropertyListing), slug } as any;
    } catch (error) {
      if (error instanceof ZodError) {
        const filteredErrors = error.errors
          .map(item => ({
            ...item,
            field: item.path[0],
          }))
          .filter((item, index, self) => index === self.findIndex(t => t.path[0] === item.path[0]));
        throw new BadRequestException(filteredErrors);
      }
      throw error;
    }
  }
  // #### UPDATE CommercialProps ########################################
  async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing, user: JwtUser): Promise<CommercialPropertyListing> {
    try {
      const [existingListing] = await this.conn.select().from(commercials_json).where(eq(commercials_json.id, id));

      if (!existingListing) {
        throw new NotFoundException(`Business listing with id ${id} not found`);
      }
      data.updated = new Date();
      data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
      if (existingListing.email === user?.email || !user) {
        data.favoritesForUser = (<CommercialPropertyListing>existingListing.data).favoritesForUser || [];
      }

      // Regenerate slug if title or location changed
      const existingData = existingListing.data as CommercialPropertyListing;
      let slug: string;
      if (data.title !== existingData.title || JSON.stringify(data.location) !== JSON.stringify(existingData.location)) {
        slug = generateSlug(data.title, data.location, id);
      } else {
        // Keep existing slug
        slug = (existingData as any).slug || generateSlug(data.title, data.location, id);
      }

      // Add slug to data before validation
      const dataWithSlug = { ...data, slug };
      CommercialPropertyListingSchema.parse(dataWithSlug);
      const imageOrder = await this.fileService.getPropertyImages(dataWithSlug.imagePath, String(dataWithSlug.serialId));
      const difference = imageOrder.filter(x => !dataWithSlug.imageOrder.includes(x)).concat(dataWithSlug.imageOrder.filter(x => !imageOrder.includes(x)));
      if (difference.length > 0) {
        this.logger.warn(`changes between image directory and imageOrder in listing ${dataWithSlug.serialId}: ${difference.join(',')}`);
        dataWithSlug.imageOrder = imageOrder;
      }
      const { id: _, email, ...rest } = dataWithSlug;
      const convertedCommercialPropertyListing = { email, data: rest };
      const [updateListing] = await this.conn.update(commercials_json).set(convertedCommercialPropertyListing).where(eq(commercials_json.id, id)).returning();
      return { id: updateListing.id, email: updateListing.email, ...(updateListing.data as CommercialPropertyListing) };
    } catch (error) {
      if (error instanceof ZodError) {
        const filteredErrors = error.errors
          .map(item => ({
            ...item,
            field: item.path[0],
          }))
          .filter((item, index, self) => index === self.findIndex(t => t.path[0] === item.path[0]));
        throw new BadRequestException(filteredErrors);
      }
      throw error;
    }
  }
  // ##############################################################
  // Images for commercial Properties
  // ##############################################################
  async deleteImage(imagePath: string, serial: string, name: string) {
    const listing = await this.findByImagePath(imagePath, serial);
    const index = listing.imageOrder.findIndex(im => im === name);
    if (index > -1) {
      listing.imageOrder.splice(index, 1);
      await this.updateCommercialPropertyListing(listing.id, listing, null);
    }
  }
  async addImage(imagePath: string, serial: string, imagename: string) {
    const listing = await this.findByImagePath(imagePath, serial);
    listing.imageOrder.push(imagename);
    await this.updateCommercialPropertyListing(listing.id, listing, null);
  }
  // #### DELETE  ########################################
  async deleteListing(id: string): Promise<void> {
    await this.conn.delete(commercials_json).where(eq(commercials_json.id, id));
  }
  // #### ADD Favorite ######################################
  async addFavorite(id: string, user: JwtUser): Promise<void> {
    await this.conn
      .update(commercials_json)
      .set({
        data: sql`jsonb_set(${commercials_json.data}, '{favoritesForUser}',
          coalesce((${commercials_json.data}->'favoritesForUser')::jsonb, '[]'::jsonb) || to_jsonb(${user.email}::text))`,
      })
      .where(eq(commercials_json.id, id));
  }
  // #### DELETE Favorite ###################################
  async deleteFavorite(id: string, user: JwtUser): Promise<void> {
    await this.conn
      .update(commercials_json)
      .set({
        data: sql`jsonb_set(${commercials_json.data}, '{favoritesForUser}',
          (SELECT coalesce(jsonb_agg(elem), '[]'::jsonb)
           FROM jsonb_array_elements(coalesce(${commercials_json.data}->'favoritesForUser', '[]'::jsonb)) AS elem
           WHERE elem::text != to_jsonb(${user.email}::text)::text))`,
      })
      .where(eq(commercials_json.id, id));
  }
}
