const axios = require('axios');
const db = require('../db/models');

const OVERPASS_URL = 'https://overpass-api.de/api/interpreter';

const OSM_CATEGORY_LABELS = {
  restaurant: 'Restoran',
  cafe: 'Cafe',
  hospital: 'Rumah Sakit',
  pharmacy: 'Apotek',
  bank: 'Bank',
  atm: 'ATM',
  fuel: 'SPBU',
  school: 'Sekolah',
  university: 'Kampus',
  hotel: 'Hotel',
};

const DEFAULT_CITIES = [
  'Jakarta',
  'Bandung',
  'Surabaya',
  'Medan',
  'Semarang',
  'Makassar',
  'Palembang',
  'Yogyakarta',
  'Bekasi',
  'Depok',
];

const CONTENT_FIELDS = [
  'name',
  'nama',
  'kategori',
  'subkategori',
  'short_description',
  'full_description',
  'address',
  'alamat',
  'kelurahan',
  'kecamatan',
  'city',
  'kota',
  'province',
  'provinsi',
  'postal_code',
  'kode_pos',
  'latitude',
  'longitude',
  'phone_number',
  'telepon',
  'whatsapp_number',
  'whatsapp',
  'email',
  'website_url',
  'website',
  'google_maps_url',
  'jam_buka',
  'status',
  'is_verified',
  'verified',
  'featured',
  'popularitas',
];

const METADATA_FIELDS = [
  'sumber',
  'external_id',
  'external_type',
  'raw_source_data',
  'last_synced_at',
];

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const normalizeList = (value, fallback) => {
  if (Array.isArray(value)) {
    const list = value.map((item) => String(item).trim()).filter(Boolean);
    return list.length ? list : fallback;
  }

  if (typeof value === 'string') {
    const list = value.split(',').map((item) => item.trim()).filter(Boolean);
    return list.length ? list : fallback;
  }

  return fallback;
};

const toPositiveInteger = (value, fallback, max) => {
  const number = Number(value);
  if (!Number.isFinite(number) || number < 0) {
    return fallback;
  }

  const integer = Math.floor(number);
  if (max === undefined) {
    return integer;
  }

  return Math.min(integer, max);
};

const toBoolean = (value, fallback = false) => {
  if (value === undefined || value === null) {
    return fallback;
  }

  return value === true || value === 'true' || value === '1' || value === 1;
};

const titleCase = (value) => String(value)
  .split('_')
  .filter(Boolean)
  .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
  .join(' ');

const amenityKeyFromInput = (value) => {
  const normalized = String(value).trim().toLowerCase();
  if (!normalized) {
    return null;
  }

  if (OSM_CATEGORY_LABELS[normalized]) {
    return normalized;
  }

  const matched = Object.entries(OSM_CATEGORY_LABELS).find((entry) => (
    entry[1].toLowerCase() === normalized
  ));

  return matched ? matched[0] : normalized.replace(/\s+/g, '_');
};

const normalizeAmenities = (params) => {
  const requested = normalizeList(
    params.amenities || params.categories,
    Object.keys(OSM_CATEGORY_LABELS),
  );

  return [...new Set(requested.map(amenityKeyFromInput).filter(Boolean))];
};

const escapeOverpassString = (value) => String(value)
  .replace(/\\/g, '\\\\')
  .replace(/"/g, '\\"');

const escapeRegex = (value) => String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

const buildOverpassQuery = (city, amenities, timeoutSeconds) => {
  const escapedCity = escapeOverpassString(city);
  const amenityRegex = amenities.map(escapeRegex).join('|');
  const selector = amenityRegex ? `["amenity"~"^(${amenityRegex})$"]` : '["amenity"]';

  return `
[out:json][timeout:${timeoutSeconds}];
area["name"="${escapedCity}"]->.searchArea;
(
  node${selector}(area.searchArea);
  way${selector}(area.searchArea);
  relation${selector}(area.searchArea);
);
out center tags;
`;
};

const getTag = (tags, keys) => {
  for (const key of keys) {
    const value = tags[key];
    if (value !== undefined && value !== null && String(value).trim() !== '') {
      return String(value).trim();
    }
  }

  return null;
};

const buildAddress = (tags) => {
  const fullAddress = getTag(tags, ['addr:full', 'contact:address']);
  if (fullAddress) {
    return fullAddress;
  }

  return [
    getTag(tags, ['addr:street']),
    getTag(tags, ['addr:housenumber']),
    getTag(tags, ['addr:neighbourhood', 'addr:suburb']),
  ].filter(Boolean).join(', ') || null;
};

const normalizeOsmElement = (element, city, amenities, includeUnnamed) => {
  const tags = element.tags || {};
  const amenity = tags.amenity;

  if (!amenity || !amenities.includes(amenity)) {
    return { reason: 'unsupported_amenity' };
  }

  const latitude = element.lat !== undefined ? element.lat : element.center && element.center.lat;
  const longitude = element.lon !== undefined ? element.lon : element.center && element.center.lon;

  if (latitude === undefined || latitude === null || longitude === undefined || longitude === null) {
    return { reason: 'missing_coordinates' };
  }

  const nama = getTag(tags, ['name', 'brand', 'operator']);
  if (!nama && !includeUnnamed) {
    return { reason: 'missing_name' };
  }

  const alamat = buildAddress(tags);
  const kota = getTag(tags, ['addr:city', 'is_in:city']) || city;
  const provinsi = getTag(tags, ['addr:province', 'addr:state']);
  const kodePos = getTag(tags, ['addr:postcode']);
  const telepon = getTag(tags, ['phone', 'contact:phone']);
  const whatsapp = getTag(tags, ['whatsapp', 'contact:whatsapp']);
  const website = getTag(tags, ['website', 'contact:website', 'url']);
  const email = getTag(tags, ['email', 'contact:email']);
  const jamBuka = getTag(tags, ['opening_hours']);
  const displayName = nama || `${titleCase(amenity)} ${city}`;
  const now = new Date();

  return {
    place: {
      name: displayName,
      nama: displayName,
      kategori: OSM_CATEGORY_LABELS[amenity] || titleCase(amenity),
      subkategori: amenity,
      short_description: `${OSM_CATEGORY_LABELS[amenity] || titleCase(amenity)} dari OpenStreetMap`,
      address: alamat,
      alamat,
      city: kota,
      kota,
      province: provinsi,
      provinsi,
      postal_code: kodePos,
      kode_pos: kodePos,
      latitude,
      longitude,
      phone_number: telepon,
      telepon,
      whatsapp_number: whatsapp,
      whatsapp,
      email,
      website_url: website,
      website,
      jam_buka: jamBuka,
      status: 'aktif',
      is_verified: false,
      verified: false,
      featured: false,
      popularitas: 0,
      sumber: 'OpenStreetMap',
      external_id: String(element.id),
      external_type: element.type,
      raw_source_data: element,
      last_synced_at: now,
    },
  };
};

const compactForUpdate = (payload, currentUser) => {
  const updatePayload = {};

  for (const field of CONTENT_FIELDS) {
    const value = payload[field];
    if (value !== undefined && value !== null && value !== '') {
      updatePayload[field] = value;
    }
  }

  for (const field of METADATA_FIELDS) {
    updatePayload[field] = payload[field];
  }

  updatePayload.updated_at = new Date();
  updatePayload.updatedById = currentUser && currentUser.id ? currentUser.id : null;

  return updatePayload;
};

module.exports = class GeoSeekCollectorService {
  static supportedAmenities() {
    return Object.entries(OSM_CATEGORY_LABELS).map(([value, label]) => ({ value, label }));
  }

  static async fetchOpenStreetMapCity(city, amenities, options) {
    const timeoutSeconds = options.timeoutSeconds;
    const timeoutMs = timeoutSeconds * 1000 + 15000;
    const query = buildOverpassQuery(city, amenities, timeoutSeconds);

    try {
      const response = await axios.get(options.overpassUrl, {
        params: { data: query },
        timeout: timeoutMs,
        maxBodyLength: Infinity,
        maxContentLength: Infinity,
      });

      const data = response.data || {};
      if (!Array.isArray(data.elements)) {
        console.error('Invalid OpenStreetMap Overpass response', { city, data });
        throw new Error(`Invalid OpenStreetMap Overpass response for ${city}`);
      }

      return data.elements;
    } catch (error) {
      console.error('OpenStreetMap Overpass request failed', {
        city,
        query,
        status: error.response && error.response.status,
        response: error.response && error.response.data,
        message: error.message,
      });
      throw error;
    }
  }

  static async upsertPlace(payload, currentUser, transaction) {
    const where = {
      sumber: payload.sumber,
      external_type: payload.external_type,
      external_id: payload.external_id,
    };

    const existing = await db.places.findOne({ where, transaction });

    if (existing) {
      await existing.update(compactForUpdate(payload, currentUser), { transaction });
      return 'updated';
    }

    try {
      await db.places.create({
        ...payload,
        createdById: currentUser && currentUser.id ? currentUser.id : null,
        updatedById: currentUser && currentUser.id ? currentUser.id : null,
      }, { transaction });
      return 'inserted';
    } catch (error) {
      if (error.name !== 'SequelizeUniqueConstraintError') {
        throw error;
      }

      const duplicate = await db.places.findOne({ where, transaction });
      if (!duplicate) {
        throw error;
      }

      await duplicate.update(compactForUpdate(payload, currentUser), { transaction });
      return 'updated';
    }
  }

  static async collectOpenStreetMap(params = {}, currentUser) {
    const cities = normalizeList(params.cities || params.kota, DEFAULT_CITIES);
    const amenities = normalizeAmenities(params);
    const includeUnnamed = toBoolean(params.includeUnnamed, false);
    const dryRun = toBoolean(params.dryRun, false);
    const delayMs = toPositiveInteger(params.delayMs, 3000, 30000);
    const timeoutSeconds = toPositiveInteger(params.timeoutSeconds, 60, 180) || 60;
    const limitPerCity = toPositiveInteger(params.limitPerCity, 0, 5000);
    const overpassUrl = params.overpassUrl || OVERPASS_URL;

    const summary = {
      success: true,
      source: 'OpenStreetMap',
      dryRun,
      citiesRequested: cities,
      amenities,
      inserted: 0,
      updated: 0,
      skipped: 0,
      skippedByReason: {},
      fetched: 0,
      processed: 0,
      cityResults: [],
    };

    for (let index = 0; index < cities.length; index += 1) {
      const city = cities[index];
      const elements = await GeoSeekCollectorService.fetchOpenStreetMapCity(city, amenities, {
        overpassUrl,
        timeoutSeconds,
      });

      summary.fetched += elements.length;

      const cityResult = {
        city,
        fetched: elements.length,
        processed: 0,
        inserted: 0,
        updated: 0,
        skipped: 0,
        skippedByReason: {},
        preview: [],
      };

      const places = [];
      for (const element of elements) {
        const normalized = normalizeOsmElement(element, city, amenities, includeUnnamed);
        if (!normalized.place) {
          const reason = normalized.reason || 'unknown';
          cityResult.skipped += 1;
          cityResult.skippedByReason[reason] = (cityResult.skippedByReason[reason] || 0) + 1;
          continue;
        }

        places.push(normalized.place);
      }

      const selectedPlaces = limitPerCity > 0 ? places.slice(0, limitPerCity) : places;
      const limited = places.length - selectedPlaces.length;
      if (limited > 0) {
        cityResult.skipped += limited;
        cityResult.skippedByReason.limit_per_city = limited;
      }

      if (dryRun) {
        cityResult.processed = selectedPlaces.length;
        cityResult.preview = selectedPlaces.slice(0, 10).map((place) => ({
          nama: place.nama,
          kategori: place.kategori,
          kota: place.kota,
          latitude: place.latitude,
          longitude: place.longitude,
          external_id: place.external_id,
          external_type: place.external_type,
        }));
      } else {
        await db.sequelize.transaction(async (transaction) => {
          for (const place of selectedPlaces) {
            const action = await GeoSeekCollectorService.upsertPlace(place, currentUser, transaction);
            if (action === 'inserted') {
              cityResult.inserted += 1;
            } else {
              cityResult.updated += 1;
            }
            cityResult.processed += 1;
          }
        });
      }

      summary.inserted += cityResult.inserted;
      summary.updated += cityResult.updated;
      summary.skipped += cityResult.skipped;
      summary.processed += cityResult.processed;
      for (const [reason, count] of Object.entries(cityResult.skippedByReason)) {
        summary.skippedByReason[reason] = (summary.skippedByReason[reason] || 0) + count;
      }
      summary.cityResults.push(cityResult);

      if (delayMs > 0 && index < cities.length - 1) {
        await sleep(delayMs);
      }
    }

    return summary;
  }
};
