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

const COLLECTION_NAME = 'listings';

/**
 * Vietnamese district abbreviation synonyms — maps common shortened forms
 * to their full diacritic variants so users can search either way.
 */
const VIETNAMESE_SYNONYMS: Array<{ id: string; synonyms: string[] }> = [
  { id: 'q1', synonyms: ['q1', 'quan 1', 'quận 1', 'q.1'] },
  { id: 'q2', synonyms: ['q2', 'quan 2', 'quận 2', 'q.2', 'thu duc', 'thủ đức'] },
  { id: 'q3', synonyms: ['q3', 'quan 3', 'quận 3', 'q.3'] },
  { id: 'q4', synonyms: ['q4', 'quan 4', 'quận 4', 'q.4'] },
  { id: 'q5', synonyms: ['q5', 'quan 5', 'quận 5', 'q.5'] },
  { id: 'q6', synonyms: ['q6', 'quan 6', 'quận 6', 'q.6'] },
  { id: 'q7', synonyms: ['q7', 'quan 7', 'quận 7', 'q.7'] },
  { id: 'q8', synonyms: ['q8', 'quan 8', 'quận 8', 'q.8'] },
  { id: 'q9', synonyms: ['q9', 'quan 9', 'quận 9', 'q.9'] },
  { id: 'q10', synonyms: ['q10', 'quan 10', 'quận 10', 'q.10'] },
  { id: 'q11', synonyms: ['q11', 'quan 11', 'quận 11', 'q.11'] },
  { id: 'q12', synonyms: ['q12', 'quan 12', 'quận 12', 'q.12'] },
  { id: 'binh-thanh', synonyms: ['binh thanh', 'bình thạnh', 'bt'] },
  { id: 'tan-binh', synonyms: ['tan binh', 'tân bình', 'tb'] },
  { id: 'tan-phu', synonyms: ['tan phu', 'tân phú', 'tp'] },
  { id: 'phu-nhuan', synonyms: ['phu nhuan', 'phú nhuận', 'pn'] },
  { id: 'go-vap', synonyms: ['go vap', 'gò vấp', 'gv'] },
  { id: 'binh-tan', synonyms: ['binh tan', 'bình tân'] },
  { id: 'nha-be', synonyms: ['nha be', 'nhà bè'] },
  { id: 'can-gio', synonyms: ['can gio', 'cần giờ'] },
  { id: 'cu-chi', synonyms: ['cu chi', 'củ chi'] },
  { id: 'hoc-mon', synonyms: ['hoc mon', 'hóc môn'] },
  { id: 'binh-chanh', synonyms: ['binh chanh', 'bình chánh'] },
  { id: 'can-ho', synonyms: ['can ho', 'căn hộ', 'chung cu', 'chung cư'] },
  { id: 'nha-pho', synonyms: ['nha pho', 'nhà phố'] },
  { id: 'biet-thu', synonyms: ['biet thu', 'biệt thự'] },
  { id: 'dat-nen', synonyms: ['dat nen', 'đất nền'] },
  { id: 'hcm', synonyms: ['hcm', 'ho chi minh', 'hồ chí minh', 'tp hcm', 'tphcm', 'sai gon', 'sài gòn'] },
];

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: 'legalStatus', type: 'string', facet: true, optional: true },
    { name: 'amenities', type: 'string[]', facet: true, optional: true },
    { name: 'isFeatured', type: 'int32', facet: true },

    // Vietnamese diacritic-normalized fields (ASCII-only, for accent-insensitive search)
    { name: 'titleNormalized', type: 'string', facet: false },
    { name: 'descriptionNormalized', type: 'string', facet: false },
    { name: 'addressNormalized', type: 'string', facet: false },
    { name: 'wardNormalized', type: 'string', facet: false },
    { name: 'districtNormalized', type: 'string', facet: false },
    { name: 'cityNormalized', type: 'string', facet: false },
    { name: 'projectNameNormalized', type: 'string', facet: false, optional: 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');
    }
    await this.ensureSynonyms();
  }

  /**
   * Upsert Vietnamese district/property-type synonyms into the collection.
   * Idempotent — safe to call on every startup.
   */
  async ensureSynonyms(): Promise<void> {
    try {
      for (const syn of VIETNAMESE_SYNONYMS) {
        await this.client
          .collections(COLLECTION_NAME)
          .synonyms()
          .upsert(syn.id, { synonyms: syn.synonyms });
      }
      this.logger.log(
        `Upserted ${VIETNAMESE_SYNONYMS.length} Vietnamese synonym rules`,
        'TypesenseSearch',
      );
    } catch (err) {
      this.logger.warn(
        `Failed to upsert synonyms: ${err instanceof Error ? err.message : String(err)}`,
        '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 rawQuery = params.query || '*';
    // For non-wildcard queries, also search the normalized (ASCII) form
    // so "can ho" matches "căn hộ" via the normalized fields.
    const normalizedQuery = rawQuery !== '*' ? Address.normalize(rawQuery) : rawQuery;
    const effectiveQuery = rawQuery !== '*' && normalizedQuery !== rawQuery
      ? `${rawQuery} ${normalizedQuery}`
      : rawQuery;

    const searchParams = {
      q: effectiveQuery,
      query_by: 'title,description,address,district,city,projectName,titleNormalized,descriptionNormalized,addressNormalized,districtNormalized,cityNormalized,projectNameNormalized',
      query_by_weights: '5,3,2,2,1,2,5,3,2,2,1,2',
      filter_by: filterBy,
      sort_by: this.buildSortBy(params),
      page,
      per_page: perPage,
      num_typos: '2',
      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`;
    }
  }
}
