import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, Subject, throwError } from 'rxjs';
import { LocationDTO } from '../Models/location.model';
import { LocationRequest } from '../Models/location-request.model';
import { tap, catchError, shareReplay } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class LocationsService {
  private baseUrl = 'http://localhost:8080/api/locations';
  private locationDetailsCache = new Map<number, Observable<LocationDTO>>();
  private mapDataCache = new Map<string, Observable<LocationDTO[]>>();
  private locationCreatedSubject = new Subject<LocationDTO>();
  private locationUpdatedSubject = new Subject<LocationDTO>();
  private locationDeletedSubject = new Subject<number>();
  public locationCreated$ = this.locationCreatedSubject.asObservable();
  public locationUpdated$ = this.locationUpdatedSubject.asObservable();
  public locationDeleted$ = this.locationDeletedSubject.asObservable();

  constructor(private http: HttpClient) {}

  getLocationsByMap(mapId: number): Observable<LocationDTO[]> {
    return this.getLocationsByMapWithGeometry(mapId, false);
  }

  getMapData(mapId: number, includeGeometry: boolean = true, includeContents: boolean = true): Observable<LocationDTO[]> {
    const cacheKey = this.buildMapDataCacheKey(mapId, includeGeometry, includeContents);
    const cached = this.mapDataCache.get(cacheKey);
    if (cached) return cached;

    const url = this.buildMapDataUrl(mapId, includeGeometry, includeContents);
    const request$ = this.http.get<LocationDTO[]>(url).pipe(
      shareReplay(1),
      catchError((error) => {
        this.mapDataCache.delete(cacheKey);
        return throwError(() => error);
      })
    );

    this.mapDataCache.set(cacheKey, request$);
    return request$;
  }

  getLocationsByMapWithGeometry(mapId: number, includeGeometry: boolean = false): Observable<LocationDTO[]> {
    return this.getMapData(mapId, includeGeometry, true);
  }

  getLocationsByParent(parentId: number): Observable<LocationDTO[]> {
    return this.http.get<LocationDTO[]>(`${this.baseUrl}/parent/${parentId}`);
  }

  getCountryLocations(mapId: number): Observable<LocationDTO[]> {
    return this.http.get<LocationDTO[]>(`${this.baseUrl}/map/${mapId}/countries`);
  }

  getCountryWithLocations(countryId: number): Observable<LocationDTO> {
    return this.http.get<LocationDTO>(`${this.baseUrl}/country/${countryId}/locations`);
  }

  createLocation(locationRequest: LocationRequest): Observable<LocationDTO> {
    return this.http.post<LocationDTO>(this.baseUrl, locationRequest).pipe(
      tap(location => {
        this.invalidateMapDataCache(location?.mapId ?? locationRequest?.location?.mapId ?? null);
        // Broadcast that a location was created
        this.locationCreatedSubject.next(location);
      })
    );
  }

  updateLocation(id: number, locationRequest: LocationRequest): Observable<LocationDTO> {
    return this.http.put<LocationDTO>(`${this.baseUrl}/${id}`, locationRequest).pipe(
      tap(location => {
        this.invalidateMapDataCache(location?.mapId ?? locationRequest?.location?.mapId ?? null);
        // invalidate cached details for this id
        if (id != null) this.locationDetailsCache.delete(id);
        this.locationUpdatedSubject.next(location);
      })
    );
  }

  deleteLocation(id: number): Observable<void> {
    return this.http.delete<void>(`${this.baseUrl}/${id}`).pipe(
      tap(() => {
        this.invalidateMapDataCache();
        // clear cached details for deleted location
        this.locationDetailsCache.delete(id);
        this.locationDeletedSubject.next(id);
      })
    );
  }

  clearAllCountryData(countryId: number): Observable<void> {
    return this.http.delete<void>(`${this.baseUrl}/country/${countryId}/clear-all`).pipe(
      tap(() => this.invalidateMapDataCache())
    );
  }

  createOrGetCountry(countryDto: any): Observable<any> {
    return this.http.post<any>(`${this.baseUrl}/country`, countryDto);
  }

  bulkCreateDistricts(countryId: number, districts: LocationRequest[]): Observable<void> {
    return this.http.post<void>(`${this.baseUrl}/country/${countryId}/bulk`, districts).pipe(
      tap(() => this.invalidateMapDataCache())
    );
  }

  bulkCreateGovernorates(districtId: number, governorates: LocationRequest[]): Observable<void> {
    return this.http.post<void>(`${this.baseUrl}/district/${districtId}/bulk`, governorates).pipe(
      tap(() => this.invalidateMapDataCache())
    );
  }

  createOrUpdateGovernorate(locationRequest: LocationRequest, parentId?: number): Observable<LocationDTO> {
    const url = parentId 
      ? `${this.baseUrl}/governorate?parentId=${parentId}` 
      : `${this.baseUrl}/governorate`;
    return this.http.post<LocationDTO>(url, locationRequest).pipe(
      tap(location => {
        this.invalidateMapDataCache(location?.mapId ?? locationRequest?.location?.mapId ?? null);
        this.locationUpdatedSubject.next(location);
      })
    );
  }

  createOrUpdateDistrict(locationRequest: LocationRequest, parentId?: number): Observable<LocationDTO> {
    const url = parentId 
      ? `${this.baseUrl}/district?parentId=${parentId}` 
      : `${this.baseUrl}/district`;
    return this.http.post<LocationDTO>(url, locationRequest).pipe(
      tap(location => {
        this.invalidateMapDataCache(location?.mapId ?? locationRequest?.location?.mapId ?? null);
        this.locationUpdatedSubject.next(location);
      })
    );
  }

  clearMapDataCache(mapId?: number): void {
    this.invalidateMapDataCache(mapId ?? null);
  }

  private buildMapDataCacheKey(mapId: number, includeGeometry: boolean, includeContents: boolean): string {
    return `${mapId}|geom=${includeGeometry}|contents=${includeContents}`;
  }

  private buildMapDataUrl(mapId: number, includeGeometry: boolean, includeContents: boolean): string {
    const params: string[] = [];
    if (includeGeometry) params.push('includeGeometry=true');
    if (includeContents) params.push('includeContents=true');
    const query = params.length > 0 ? `?${params.join('&')}` : '';
    return `${this.baseUrl}/map/${mapId}${query}`;
  }

  private invalidateMapDataCache(mapId: number | null = null): void {
    if (mapId == null) {
      this.mapDataCache.clear();
      return;
    }

    const prefix = `${mapId}|`;
    Array.from(this.mapDataCache.keys())
      .filter(key => key.startsWith(prefix))
      .forEach(key => this.mapDataCache.delete(key));
  }

  // ==================== TUNISIA GEOJSON IMPORT ====================
  
  /**
   * Import Tunisia districts and governorates from GeoJSON file in backend
   */
  importTunisiaData(mapId: number): Observable<string> {
    return this.http.post(`${this.baseUrl}/tunisia/import?mapId=${mapId}`, null, { responseType: 'text' });
  }

  /**
   * Force reimport Tunisia data (deletes existing first)
   */
  reimportTunisiaData(mapId: number): Observable<string> {
    return this.http.post(`${this.baseUrl}/tunisia/reimport?mapId=${mapId}`, null, { responseType: 'text' });
  }

  /**
   * Get all districts for a map with their geometries
   */
  getDistricts(mapId: number): Observable<LocationDTO[]> {
    return this.http.get<LocationDTO[]>(`${this.baseUrl}/map/${mapId}/districts`);
  }

  /**
   * Get all governorates for a map with their geometries
   */
  getGovernorates(mapId: number): Observable<LocationDTO[]> {
    return this.http.get<LocationDTO[]>(`${this.baseUrl}/map/${mapId}/governorates`);
  }

  /**
   * Get governorates for a specific district
   */
  getGovernoratesByDistrict(districtId: number): Observable<LocationDTO[]> {
    return this.http.get<LocationDTO[]>(`${this.baseUrl}/district/${districtId}/governorates`);
  }

  // ==================== DYNAMIC IMPORT & HIERARCHICAL DATA ====================

  /**
   * Dynamically import districts and governorates from a mapping.
   * 
   * Example request:
   * {
   *   mapId: 1,
   *   districts: {
   *     district1: {
   *       name_ar: "الإقليم الأول",
   *       name_en: "First District",
   *       name_fr: "Premier District",
   *       members: ["Bizerte", "Beja", "Jendouba", "Le Kef"],
   *       members_ar: ["بنزرت", "باجة", "جندوبة", "الكاف"]
   *     },
   *     district2: { ... }
   *   },
   *   loadGeometriesFromGeoJson: true,
   *   geoJsonPath: "static/tunisia_adm1.geojson"
   * }
   */
  importDynamicDistricts(request: DynamicImportRequest): Observable<string> {
    return this.http.post(`${this.baseUrl}/dynamic/import`, request, { responseType: 'text' });
  }

  /**
   * Get districts with their child governorates (hierarchical data).
   * 
   * @param mapId - The map ID
   * @param includeGeometry - Whether to include geometry data (default: false for performance)
   * 
   * Returns a hierarchical structure:
   * [
   *   {
   *     id: 1,
   *     name: "First District",
   *     translations: { ar: "الإقليم الأول", en: "First District", fr: "Premier District" },
   *     children: [
   *       { id: 10, name: "Bizerte", translations: {...}, ... },
   *       { id: 11, name: "Beja", translations: {...}, ... }
   *     ]
   *   },
   *   ...
   * ]
   */
  getDistrictsHierarchy(mapId: number, includeGeometry: boolean = false): Observable<LocationDTO[]> {
    return this.http.get<LocationDTO[]>(`${this.baseUrl}/map/${mapId}/hierarchy?includeGeometry=${includeGeometry}`);
  }

  /**
   * Get a single location with full details (geometry, contents, translations).
   * Use this for detail views when you need everything.
   */
  getLocationDetails(id: number): Observable<LocationDTO> {
    const cached = this.locationDetailsCache.get(id);
    if (cached) return cached;

    const request$ = this.http.get<LocationDTO>(`${this.baseUrl}/${id}/details`).pipe(
      shareReplay(1),
      catchError((err) => {
        this.locationDetailsCache.delete(id);
        return throwError(() => err);
      })
    );

    this.locationDetailsCache.set(id, request$);
    return request$;
  }

  /**
   * Get governorates for a specific district with optional geometry.
   * 
   * @param districtId - The district ID
   * @param includeGeometry - Whether to include geometry data (default: true)
   */
  getGovernoratesDetailed(districtId: number, includeGeometry: boolean = true): Observable<LocationDTO[]> {
    return this.http.get<LocationDTO[]>(`${this.baseUrl}/district/${districtId}/governorates/detailed?includeGeometry=${includeGeometry}`);
  }

  // ==================== HELPER METHODS ====================

  /**
   * Build dynamic import request from district mapping object (from component).
   * Converts the TypeScript district mapping format to the backend DTO format.
   */
  buildDynamicImportRequest(mapId: number, districtsMapping: any): DynamicImportRequest {
    const request: DynamicImportRequest = {
      mapId: mapId,
      districts: {},
      loadGeometriesFromGeoJson: true,
      geoJsonPath: 'static/tunisia_adm1.geojson'
    };

    // Convert each district in the mapping
    for (const districtId in districtsMapping) {
      const meta = districtsMapping[districtId];
      request.districts[districtId] = {
        name_ar: meta.name_ar,
        name_en: meta.name_en,
        name_fr: meta.name_fr,
        members: meta.members,
        members_ar: meta.members_ar,
        members_fr: meta.members_fr || meta.members.map(() => meta.name_fr) // fallback to English if French not provided
      };
    }

    return request;
  }
}

// ==================== INTERFACES ====================

/**
 * Interface for dynamic import request
 */
export interface DynamicImportRequest {
  mapId: number;
  districts: {
    [districtId: string]: {
      name_ar: string;
      name_en: string;
      name_fr: string;
      members: string[];
      members_ar: string[];
      members_fr?: string[];
    };
  };
  loadGeometriesFromGeoJson: boolean;
  geoJsonPath: string;
}
