import { env } from '../../../config/env';
import { redis } from '../../../config/redis';
import { logger } from '../../../utils/logger';

export interface CandidateLocation {
  latitude: number;
  longitude: number;
  address: string;
  postalCode?: string;
  city?: string;
  province?: string;
  source: 'osm' | 'nar' | 'reverse-geocode';
  confidence: number;
  priority: number; // NAR=3, OSM=2, ReverseGeocode=1
}

interface OverpassElement {
  type: 'node' | 'way' | 'relation';
  id: number;
  lat?: number;
  lon?: number;
  center?: { lat: number; lon: number };
  tags?: Record<string, string>;
}

interface OverpassResponse {
  elements: OverpassElement[];
}

interface OverpassCountResponse {
  elements: { tags: { total: string } }[];
}

const REDIS_LAST_REQUEST_KEY = 'overpass:last-request';
const MAX_AREA_SQ_DEG = 0.05; // ~25 sq km — split above this

/**
 * Enforce minimum delay between Overpass API requests.
 * Uses Redis timestamp to coordinate across potential instances.
 */
async function waitForRateLimit(): Promise<void> {
  const minDelay = env.OVERPASS_MIN_DELAY_MS;
  const now = Date.now();

  const lastStr = await redis.get(REDIS_LAST_REQUEST_KEY);
  const lastRequest = lastStr ? parseInt(lastStr, 10) : 0;
  const elapsed = now - lastRequest;

  if (elapsed < minDelay) {
    const waitMs = minDelay - elapsed;
    logger.debug(`Overpass rate limit: waiting ${waitMs}ms`);
    await new Promise((resolve) => setTimeout(resolve, waitMs));
  }

  await redis.set(REDIS_LAST_REQUEST_KEY, Date.now().toString(), 'EX', 120);
}

/**
 * Execute an Overpass API query.
 */
async function queryOverpass<T>(query: string): Promise<T> {
  await waitForRateLimit();

  const url = env.OVERPASS_API_URL;
  logger.info(`Overpass query to ${url} (${query.length} chars)`);

  const response = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: `data=${encodeURIComponent(query)}`,
    signal: AbortSignal.timeout(200000), // 200s timeout (Overpass can be slow)
  });

  if (response.status === 429) {
    throw new Error('Overpass API rate limit exceeded. Try again later or use a private instance.');
  }

  if (!response.ok) {
    const text = await response.text().catch(() => '');
    throw new Error(`Overpass API error ${response.status}: ${text.substring(0, 200)}`);
  }

  return response.json() as Promise<T>;
}

/**
 * Split a bounding box into 4 quadrants.
 */
function splitBounds(bounds: { minLat: number; maxLat: number; minLng: number; maxLng: number }) {
  const midLat = (bounds.minLat + bounds.maxLat) / 2;
  const midLng = (bounds.minLng + bounds.maxLng) / 2;
  return [
    { minLat: bounds.minLat, maxLat: midLat, minLng: bounds.minLng, maxLng: midLng },
    { minLat: bounds.minLat, maxLat: midLat, minLng: midLng, maxLng: bounds.maxLng },
    { minLat: midLat, maxLat: bounds.maxLat, minLng: bounds.minLng, maxLng: midLng },
    { minLat: midLat, maxLat: bounds.maxLat, minLng: midLng, maxLng: bounds.maxLng },
  ];
}

function areaSqDeg(bounds: { minLat: number; maxLat: number; minLng: number; maxLng: number }): number {
  return (bounds.maxLat - bounds.minLat) * (bounds.maxLng - bounds.minLng);
}

/**
 * Build address string from OSM tags.
 */
function buildAddress(tags: Record<string, string>): string {
  const parts: string[] = [];

  const houseNumber = tags['addr:housenumber'];
  const street = tags['addr:street'];
  if (houseNumber) parts.push(houseNumber);
  if (street) parts.push(street);

  const city = tags['addr:city'];
  if (city) parts.push(city);

  const province = tags['addr:province'] || tags['addr:state'];
  if (province) parts.push(province);

  return parts.join(', ') || `${houseNumber || '?'} ${street || 'Unknown Street'}`;
}

/**
 * Parse Overpass elements into CandidateLocation objects.
 */
function parseElements(elements: OverpassElement[]): CandidateLocation[] {
  const candidates: CandidateLocation[] = [];

  for (const el of elements) {
    const lat = el.lat ?? el.center?.lat;
    const lon = el.lon ?? el.center?.lon;
    if (!lat || !lon) continue;

    const tags = el.tags ?? {};
    if (!tags['addr:housenumber']) continue;

    candidates.push({
      latitude: lat,
      longitude: lon,
      address: buildAddress(tags),
      postalCode: tags['addr:postcode'],
      city: tags['addr:city'],
      province: tags['addr:province'] || tags['addr:state'],
      source: 'osm',
      confidence: 70,
      priority: 2,
    });
  }

  return candidates;
}

export const overpassService = {
  /**
   * Estimate the number of address nodes in a bounding box using [out:count].
   */
  async estimateCount(bounds: { minLat: number; maxLat: number; minLng: number; maxLng: number }): Promise<number> {
    const bbox = `${bounds.minLat},${bounds.minLng},${bounds.maxLat},${bounds.maxLng}`;
    const query = `[out:json][timeout:60];(node["addr:housenumber"](${bbox});way["building"]["addr:housenumber"](${bbox}););out count;`;

    try {
      const data = await queryOverpass<OverpassCountResponse>(query);
      const totalStr = data.elements?.[0]?.tags?.total;
      return totalStr ? parseInt(totalStr, 10) : 0;
    } catch (err) {
      logger.warn('Overpass count estimate failed:', err);
      return -1; // -1 indicates unknown
    }
  },

  /**
   * Query all address data within a bounding box.
   * Automatically splits large areas into sub-quadrants.
   * Returns candidates and the number of quadrants that failed.
   */
  async queryArea(
    bounds: { minLat: number; maxLat: number; minLng: number; maxLng: number },
    onProgress?: (msg: string) => void,
  ): Promise<{ candidates: CandidateLocation[]; failedQuadrants: number }> {
    const area = areaSqDeg(bounds);

    // If area is too large, split into quadrants and query each
    if (area > MAX_AREA_SQ_DEG) {
      const quadrants = splitBounds(bounds);
      const allCandidates: CandidateLocation[] = [];
      const totalQuadrants = quadrants.length;
      let failedQuadrants = 0;

      for (let i = 0; i < totalQuadrants; i++) {
        onProgress?.(`Querying OSM quadrant ${i + 1}/${totalQuadrants}`);
        try {
          const result = await this.queryArea(quadrants[i]!, onProgress);
          for (const c of result.candidates) allCandidates.push(c);
          failedQuadrants += result.failedQuadrants;
        } catch (err) {
          const msg = err instanceof Error ? err.message : 'Unknown error';
          logger.warn(`Overpass quadrant ${i + 1} failed: ${msg}`);
          failedQuadrants++;
          // Continue with other quadrants
        }
      }

      return { candidates: allCandidates, failedQuadrants };
    }

    const bbox = `${bounds.minLat},${bounds.minLng},${bounds.maxLat},${bounds.maxLng}`;
    const query = `[out:json][timeout:180];(node["addr:housenumber"](${bbox});way["building"]["addr:housenumber"](${bbox}););out center body;`;

    onProgress?.('Querying OSM addresses...');
    const data = await queryOverpass<OverpassResponse>(query);

    return { candidates: parseElements(data.elements), failedQuadrants: 0 };
  },
};
