import httpStatus from 'http-status';
import { Types } from 'mongoose';
import { IArea, IGeoLocation } from './location.interfaces.js';
import Area from './area/area.model.js';
import City from './city/city.model.js';
import State from './state/state.model.js';
import Country from './country/country.model.js';
import ApiError from '@/shared/utils/errors/ApiError.js';
import { processPlaceId, getAreaCoordinates } from './google-places.service.js';

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

type LocationHierarchyLean = {
  _id: Types.ObjectId;
  name: string;
  loc: IGeoLocation;
};

export interface ILocationHierarchy {
  country: LocationHierarchyLean | null;
  state: LocationHierarchyLean | null;
  city: LocationHierarchyLean | null;
  area: LocationHierarchyLean | null;
}

export interface IAddressComponents {
  country: string;
  state: string;
  city: string;
  area?: string;
  pincode?: number;
  coordinates?: [number, number];
}

export interface IPlaceIdResponse {
  hierarchy: ILocationHierarchy;
  placeDetails?: {
    coordinates: { lat: number; lng: number };
    place_id: string;
    formatted_address?: string;
    pinCode: number;
  };
}

export const createLocationHierarchy = async (
  addressComponents: IAddressComponents,
  userId?: string,
): Promise<ILocationHierarchy> => {
  const {
    country: countryName,
    state: stateName,
    city: cityName,
    area: areaName,
    pincode,
    coordinates,
  } = addressComponents;
  const userIdObj: { createdBy?: Types.ObjectId; updatedBy?: Types.ObjectId } =
    userId
      ? {
          createdBy: new Types.ObjectId(userId),
          updatedBy: new Types.ObjectId(userId),
        }
      : {};

  const result: ILocationHierarchy = {
    country: null,
    state: null,
    city: null,
    area: null,
  };

  if (countryName && countryName.trim()) {
    const escapedCountryName = escapeRegex(countryName.trim());
    result.country = (await Country.findOne({
      name: { $regex: new RegExp(`^${escapedCountryName}$`, 'i') },
    })
      .select('_id name loc')
      .lean()) as LocationHierarchyLean | null;
  } else {
    throw new ApiError(httpStatus.BAD_REQUEST, 'Country name is required');
  }

  if (stateName && stateName.trim() && result.country) {
    const escapedStateName = escapeRegex(stateName.trim());
    result.state = (await State.findOne({
      name: { $regex: new RegExp(`^${escapedStateName}$`, 'i') },
    })
      .select('_id name loc')
      .lean()) as LocationHierarchyLean | null;
  } else {
    throw new ApiError(httpStatus.BAD_REQUEST, 'State name is required');
  }

  if (cityName && cityName.trim() && result.state) {
    const escapedCityName = escapeRegex(cityName.trim());
    result.city = (await City.findOne({
      name: { $regex: new RegExp(`^${escapedCityName}$`, 'i') },
    })
      .select('_id name loc')
      .lean()) as LocationHierarchyLean | null;
  } else {
    throw new ApiError(httpStatus.BAD_REQUEST, 'City name is required');
  }

  if (areaName && areaName.trim() && result.city) {
    const escapedAreaName = escapeRegex(areaName.trim());
    result.area = (await Area.findOne({
      name: { $regex: new RegExp(`^${escapedAreaName}$`, 'i') },
      city: result.city._id,
    })
      .select('_id name loc')
      .lean()) as LocationHierarchyLean | null;

    if (!result.area) {
      const areaToCreate: Partial<IArea> = {
        name: areaName,
        city: new Types.ObjectId(result.city._id.toString()),
        pinCode: pincode ? [pincode] : [],
        loc: coordinates
          ? { type: 'Point', coordinates }
          : { type: 'Point', coordinates: [0, 0] },
        ...userIdObj,
      };

      const createdArea = await Area.create(areaToCreate);
      result.area = (await Area.findById(createdArea._id)
        .select('_id name loc')
        .lean()) as LocationHierarchyLean | null;
    }
  }

  return result;
};

export const createLocationHierarchyFromPlaceId = async (
  placeId: string,
  userId?: string,
): Promise<IPlaceIdResponse> => {
  const { normalized, original } = await processPlaceId(placeId);

  if (!normalized.country || !normalized.state || !normalized.city) {
    throw new ApiError(
      httpStatus.BAD_REQUEST,
      'Place must include country, state, and city information',
    );
  }

  const addressComponents: IAddressComponents = {
    country: normalized.country,
    state: normalized.state,
    city: normalized.city,
    area: normalized.area,
    pincode: original.postal_code ? parseInt(original.postal_code) : undefined,
    coordinates: original.coordinates
      ? [original.coordinates.lat, original.coordinates.lng]
      : undefined,
  };

  const hierarchy = await createLocationHierarchy(addressComponents, userId);

  if (!hierarchy.area && hierarchy.city && normalized.area && normalized.city) {
    const areaCoords = await getAreaCoordinates(
      normalized.area,
      normalized.city,
      original.postal_code,
    );

    const userIdObj: {
      createdBy?: Types.ObjectId;
      updatedBy?: Types.ObjectId;
    } = userId
      ? {
          createdBy: new Types.ObjectId(userId),
          updatedBy: new Types.ObjectId(userId),
        }
      : {};

    const areaToCreate: Partial<IArea> = {
      name: normalized.area,
      city: new Types.ObjectId(hierarchy.city._id.toString()),
      pinCode: original.postal_code ? [parseInt(original.postal_code)] : [],
      loc: {
        type: 'Point',
        coordinates: [areaCoords.coordinates.lng, areaCoords.coordinates.lat],
      },
      ...userIdObj,
    };

    const createdArea = await Area.create(areaToCreate);
    hierarchy.area = (await Area.findById(createdArea._id)
      .select('_id name loc')
      .lean()) as LocationHierarchyLean | null;

    return {
      hierarchy,
      placeDetails: {
        ...areaCoords,
        pinCode: original.postal_code
          ? parseInt(original.postal_code)
          : undefined,
      },
    };
  }

  return {
    hierarchy,
    placeDetails: {
      coordinates: {
        lat: original.coordinates.lat,
        lng: original.coordinates.lng,
      },
      place_id: placeId,
      pinCode: original.postal_code
        ? parseInt(original.postal_code)
        : undefined,
    },
  };
};
