import { Injectable } from '@nestjs/common';
import { type Client as TypesenseClient } from 'typesense';
import { type CollectionCreateSchema } from 'typesense/lib/Typesense/Collections';
import { type LoggerService } from '@modules/shared';
import {
  type ISearchRepository,
  type ListingDocument,
  type SearchParams,
  type SearchResult,
} from '../../domain/repositories/search.repository';
import { type TypesenseClientService } from './typesense-client.service';

const COLLECTION_NAME = 'listings';

const LISTING_SCHEMA: CollectionCreateSchema = {
  name: COLLECTION_NAME,
  fields: [
    { name: 'listingId', type: 'string', facet: false },
    { name: 'propertyId', type: 'string', facet: false },
    { name: 'title', type: 'string', facet: false },
    { name: 'description', type: 'string', facet: false },
    { name: 'propertyType', type: 'string', facet: true },
    { name: 'transactionType', type: 'string', facet: true },
    { name: 'priceVND', type: 'int64', facet: false },
    { name: 'pricePerM2', type: 'float', facet: false, optional: true },
    { name: 'areaM2', type: 'float', facet: false },
    { name: 'bedrooms', type: 'int32', facet: true, optional: true },
    { name: 'bathrooms', type: 'int32', facet: true, optional: true },
    { name: 'floors', type: 'int32', facet: false, optional: true },
    { name: 'direction', type: 'string', facet: true, optional: true },
    { name: 'address', type: 'string', facet: false },
    { name: 'ward', type: 'string', facet: true },
    { name: 'district', type: 'string', facet: true },
    { name: 'city', type: 'string', facet: true },
    { name: 'location', type: 'geopoint', facet: false },
    { name: 'agentId', type: 'string', facet: false, optional: true },
    { name: 'sellerId', type: 'string', facet: false },
    { name: 'status', type: 'string', facet: true },
    { name: 'publishedAt', type: 'int64', facet: false },
    { name: 'viewCount', type: 'int32', facet: false },
    { name: 'saveCount', type: 'int32', facet: false },
    { name: 'projectName', type: 'string', facet: true, optional: true },
    { name: 'amenities', type: 'string[]', facet: true, optional: true },
    { name: 'isFeatured', type: 'int32', facet: true },
  ],
  token_separators: ['-', '_'],
  enable_nested_fields: false,
};

@Injectable()
export class TypesenseSearchRepository implements ISearchRepository {
  private get client(): TypesenseClient {
    return this.typesenseClient.getClient();
  }

  constructor(
    private readonly typesenseClient: TypesenseClientService,
    private readonly logger: LoggerService,
  ) {}

  async ensureCollection(): Promise<void> {
    try {
      await this.client.collections(COLLECTION_NAME).retrieve();
      this.logger.log(`Collection "${COLLECTION_NAME}" already exists`, 'TypesenseSearch');
    } catch {
      await this.client.collections().create(LISTING_SCHEMA);
      this.logger.log(`Collection "${COLLECTION_NAME}" created`, 'TypesenseSearch');
    }
  }

  async dropCollection(): Promise<void> {
    try {
      await this.client.collections(COLLECTION_NAME).delete();
      this.logger.log(`Collection "${COLLECTION_NAME}" dropped`, 'TypesenseSearch');
    } catch {
      this.logger.warn(`Collection "${COLLECTION_NAME}" not found to drop`, 'TypesenseSearch');
    }
  }

  async indexDocument(doc: ListingDocument): Promise<void> {
    await this.client
      .collections(COLLECTION_NAME)
      .documents()
      .upsert(doc as unknown as Record<string, unknown>);
  }

  async indexDocuments(docs: ListingDocument[]): Promise<void> {
    if (docs.length === 0) return;

    const results = await this.client
      .collections(COLLECTION_NAME)
      .documents()
      .import(docs as unknown as Record<string, unknown>[], { action: 'upsert' });

    const failures = results.filter((r: { success: boolean }) => !r.success);
    if (failures.length > 0) {
      this.logger.warn(
        `${failures.length}/${docs.length} documents failed to index`,
        'TypesenseSearch',
      );
    }
  }

  async removeDocument(id: string): Promise<void> {
    try {
      await this.client.collections(COLLECTION_NAME).documents(id).delete();
    } catch {
      this.logger.warn(`Document ${id} not found for removal`, 'TypesenseSearch');
    }
  }

  async search(params: SearchParams): Promise<SearchResult> {
    const page = params.page ?? 1;
    const perPage = params.perPage ?? 20;

    let filterBy = params.filterBy || '';

    if (params.geoPoint && params.geoRadiusKm) {
      const geoFilter = `location:(${params.geoPoint.lat}, ${params.geoPoint.lng}, ${params.geoRadiusKm} km)`;
      filterBy = filterBy ? `${filterBy} && ${geoFilter}` : geoFilter;
    }

    const searchParams = {
      q: params.query || '*',
      query_by: 'title,description,address,district,city,projectName',
      query_by_weights: '5,3,2,2,1,2',
      filter_by: filterBy,
      sort_by: this.buildSortBy(params),
      page,
      per_page: perPage,
      highlight_full_fields: 'title,description',
      highlight_start_tag: '<mark>',
      highlight_end_tag: '</mark>',
    };

    const result = await this.client
      .collections(COLLECTION_NAME)
      .documents()
      .search(searchParams);

    const hits = (result.hits ?? []).map(
      (hit) => hit.document as unknown as ListingDocument,
    );
    const totalFound = (result.found as number) ?? 0;

    return {
      hits,
      totalFound,
      page,
      perPage,
      totalPages: Math.ceil(totalFound / perPage),
      searchTimeMs: result.search_time_ms ?? 0,
    };
  }

  private buildSortBy(params: SearchParams): string {
    if (params.geoPoint) {
      if (params.sortBy === 'distance' || (!params.sortBy && params.geoRadiusKm)) {
        return `location(${params.geoPoint.lat}, ${params.geoPoint.lng}):asc`;
      }
    }

    // Featured listings always sort first, then by user-selected criteria
    const featuredPrefix = 'isFeatured:desc';

    switch (params.sortBy) {
      case 'price_asc':
        return `${featuredPrefix},priceVND:asc`;
      case 'price_desc':
        return `${featuredPrefix},priceVND:desc`;
      case 'date_desc':
        return `${featuredPrefix},publishedAt:desc`;
      case 'relevance':
      default:
        return params.query && params.query !== '*'
          ? `${featuredPrefix},_text_match:desc,publishedAt:desc`
          : `${featuredPrefix},publishedAt:desc`;
    }
  }
}
