import { html, type TemplateResult } from '@design.estate/dees-element';
import { renderIcon } from './geo-map.icons.js';

/**
 * Result from Nominatim geocoding API
 */
export interface INominatimResult {
  place_id: number;
  licence: string;
  osm_type: string;
  osm_id: number;
  boundingbox: [string, string, string, string]; // [south, north, west, east]
  lat: string;
  lon: string;
  display_name: string;
  class: string;
  type: string;
  importance: number;
}

/**
 * Event fired when an address is selected from search results
 */
export interface IAddressSelectedEvent {
  address: string;
  coordinates: [number, number]; // [lng, lat]
  boundingBox: [number, number, number, number]; // [south, north, west, east]
  placeId: string;
  type: string;
}

/**
 * Configuration for SearchController
 */
export interface ISearchControllerConfig {
  placeholder?: string;
  debounceMs?: number;
  minQueryLength?: number;
  maxResults?: number;
}

/**
 * Callback interface for SearchController events
 */
export interface ISearchControllerCallbacks {
  onResultSelected: (result: INominatimResult, coordinates: [number, number], zoom: number) => void;
  onRequestUpdate: () => void;
}

/**
 * Reusable search controller for Nominatim geocoding
 * Can be used for standalone search or within navigation inputs
 */
export class SearchController {
  // State
  public query: string = '';
  public results: INominatimResult[] = [];
  public isOpen: boolean = false;
  public highlightedIndex: number = -1;
  public isSearching: boolean = false;

  // Config
  private placeholder: string;
  private debounceMs: number;
  private minQueryLength: number;
  private maxResults: number;

  // Internal
  private debounceTimer: ReturnType<typeof setTimeout> | null = null;
  private callbacks: ISearchControllerCallbacks;
  private boundClickOutsideHandler: ((e: MouseEvent) => void) | null = null;

  constructor(config: ISearchControllerConfig, callbacks: ISearchControllerCallbacks) {
    this.placeholder = config.placeholder ?? 'Search address...';
    this.debounceMs = config.debounceMs ?? 500;
    this.minQueryLength = config.minQueryLength ?? 3;
    this.maxResults = config.maxResults ?? 5;
    this.callbacks = callbacks;
  }

  /**
   * Search Nominatim API for addresses
   */
  public async search(query: string): Promise<INominatimResult[]> {
    if (query.length < this.minQueryLength) return [];

    const params = new URLSearchParams({
      q: query,
      format: 'json',
      limit: String(this.maxResults),
      addressdetails: '1',
    });

    const response = await fetch(`https://nominatim.openstreetmap.org/search?${params}`, {
      headers: {
        'User-Agent': 'dees-geo-map/1.0',
      },
    });

    if (!response.ok) return [];
    return response.json();
  }

  /**
   * Handle input event from search input
   */
  public handleInput(event: Event): void {
    const input = event.target as HTMLInputElement;
    this.query = input.value;
    this.highlightedIndex = -1;

    if (this.debounceTimer) {
      clearTimeout(this.debounceTimer);
    }

    if (this.query.length < this.minQueryLength) {
      this.results = [];
      this.isOpen = false;
      this.isSearching = false;
      this.callbacks.onRequestUpdate();
      return;
    }

    this.isSearching = true;
    this.callbacks.onRequestUpdate();

    this.debounceTimer = setTimeout(async () => {
      const results = await this.search(this.query);
      this.results = results;
      this.isOpen = results.length > 0 || this.query.length >= this.minQueryLength;
      this.isSearching = false;
      this.callbacks.onRequestUpdate();
    }, this.debounceMs);
  }

  /**
   * Handle keyboard navigation in search results
   */
  public handleKeydown(event: KeyboardEvent): void {
    if (!this.isOpen) return;

    switch (event.key) {
      case 'ArrowDown':
        event.preventDefault();
        this.highlightedIndex = Math.min(
          this.highlightedIndex + 1,
          this.results.length - 1
        );
        this.callbacks.onRequestUpdate();
        break;
      case 'ArrowUp':
        event.preventDefault();
        this.highlightedIndex = Math.max(this.highlightedIndex - 1, -1);
        this.callbacks.onRequestUpdate();
        break;
      case 'Enter':
        event.preventDefault();
        if (this.highlightedIndex >= 0 && this.results[this.highlightedIndex]) {
          this.selectResult(this.results[this.highlightedIndex]);
        }
        break;
      case 'Escape':
        event.preventDefault();
        this.close();
        break;
    }
  }

  /**
   * Select a search result
   */
  public selectResult(result: INominatimResult): void {
    const lng = parseFloat(result.lon);
    const lat = parseFloat(result.lat);
    const coordinates: [number, number] = [lng, lat];
    const zoom = this.calculateZoomForResult(result);

    this.callbacks.onResultSelected(result, coordinates, zoom);
    this.close();
  }

  /**
   * Handle focus on search input
   */
  public handleFocus(): void {
    if (this.results.length > 0 || this.query.length >= this.minQueryLength) {
      this.isOpen = true;
      this.callbacks.onRequestUpdate();
    }
  }

  /**
   * Clear search state
   */
  public clear(): void {
    this.close();
  }

  /**
   * Close search dropdown
   */
  public close(): void {
    this.isOpen = false;
    this.results = [];
    this.query = '';
    this.highlightedIndex = -1;
    this.callbacks.onRequestUpdate();
  }

  /**
   * Set query without triggering search (for external updates)
   */
  public setQuery(query: string): void {
    this.query = query;
    this.results = [];
    this.isOpen = false;
    this.callbacks.onRequestUpdate();
  }

  /**
   * Calculate appropriate zoom level based on result type
   */
  public calculateZoomForResult(result: INominatimResult): number {
    const type = result.type;
    const osmClass = result.class;

    // Zoom levels based on place type
    if (osmClass === 'boundary' && type === 'administrative') {
      // Use importance to determine administrative level
      if (result.importance > 0.8) return 5; // Country
      if (result.importance > 0.6) return 7; // State/Region
      if (result.importance > 0.4) return 10; // County
      return 12; // City/Town
    }

    const zoomByType: Record<string, number> = {
      country: 5,
      state: 7,
      region: 7,
      county: 10,
      city: 12,
      town: 13,
      village: 14,
      suburb: 14,
      neighbourhood: 15,
      street: 16,
      road: 16,
      house: 18,
      building: 18,
    };

    return zoomByType[type] ?? 15;
  }

  /**
   * Render the search component
   * @param extraClass - Optional CSS class to add to the container for positioning
   */
  public render(extraClass?: string): TemplateResult {
    return html`
      <div class="search-container ${extraClass || ''}">
        <div class="search-input-wrapper">
          <div class="search-icon">
            ${renderIcon('search')}
          </div>
          <input
            type="text"
            class="search-input"
            placeholder="${this.placeholder}"
            .value=${this.query}
            @input=${(e: Event) => this.handleInput(e)}
            @keydown=${(e: KeyboardEvent) => this.handleKeydown(e)}
            @focus=${() => this.handleFocus()}
          />
          ${this.isSearching ? html`
            <div class="search-spinner">
              ${renderIcon('spinner')}
            </div>
          ` : this.query ? html`
            <button class="search-clear" @click=${() => this.clear()} title="Clear">
              ${renderIcon('close')}
            </button>
          ` : ''}
        </div>
        ${this.isOpen ? html`
          <div class="search-results">
            ${this.results.length > 0 ? this.results.map((result, index) => html`
              <div
                class="search-result ${index === this.highlightedIndex ? 'highlighted' : ''}"
                @click=${() => this.selectResult(result)}
                @mouseenter=${() => { this.highlightedIndex = index; this.callbacks.onRequestUpdate(); }}
              >
                <span class="search-result-name">${result.display_name}</span>
                <span class="search-result-type">${result.type}</span>
              </div>
            `) : this.query.length >= this.minQueryLength && !this.isSearching ? html`
              <div class="search-no-results">No results found</div>
            ` : ''}
          </div>
        ` : ''}
      </div>
    `;
  }
}
