import { Op } from 'sequelize';

import { Optional } from 'src/types';
import { Db } from 'src/db';
import { ValidationError } from '../errors';

type Order = 'ASC' | 'DESC';

class LocationRequest {
  userId: Optional<number> = null;
  deviceId: Optional<string> = null;
  limit: Optional<number> = 250;
  offset: Optional<number> = null;
  startDate: Optional<Date> = null;
  endDate: Optional<Date> = null;
  ids: Optional<number[]> = null;
  minId: Optional<number> = null;
  maxId: Optional<number> = null;
  minLatitude: Optional<number> = null;
  maxLatitude: Optional<number> = null;
  minLongitude: Optional<number> = null;
  maxLongitude: Optional<number> = null;
  country: Optional<string> = null;
  locality: Optional<string> = null;
  postalCode: Optional<string> = null;
  description: Optional<string> = null;
  orderBy: string = 'timestamp';
  order: Order = 'DESC';

  constructor(req: {
    userId?: number;
    deviceId?: string;
    limit?: number;
    offset?: number;
    startDate?: Date;
    endDate?: Date;
    ids?: number[] | string;
    minId?: number;
    maxId?: number;
    minLatitude?: number;
    maxLatitude?: number;
    minLongitude?: number;
    maxLongitude?: number;
    country?: string;
    locality?: string;
    postalCode?: string;
    description?: string;
    orderBy?: string;
    order?: string;
  }) {
    this.userId = req.userId;
    this.deviceId = req.deviceId?.length ? req.deviceId : this.deviceId;
    this.initNumber('limit', req);
    this.initNumber('offset', req);
    this.initDate('startDate', req);
    this.initDate('endDate', req);
    this.initNumber('minId', req);
    this.initNumber('maxId', req);
    this.initNumber('minLatitude', req, parseFloat);
    this.initNumber('maxLatitude', req, parseFloat);
    this.initNumber('minLongitude', req, parseFloat);
    this.initNumber('maxLongitude', req, parseFloat);
    this.country = req.country?.toLowerCase();
    this.locality = req.locality;
    this.postalCode = req.postalCode;
    this.description = req.description;
    this.orderBy = req.orderBy || this.orderBy;
    this.order = (req.order || this.order).toUpperCase() as Order;

    const ids = typeof req.ids === 'string' ? req.ids.split(/\s*,\s*/) : req.ids;
    this.ids = (ids || []).map((id: any) => {
      const numId = parseInt(id);
      if (isNaN(numId)) {
        throw new ValidationError(`Invalid value for ids: ${id}`);
      }
      return numId;
    });
  }

  private initNumber(key: string, req: any, parser: (s: string) => number = parseInt): void {
    if (req[key] != null) {
      const numValue = (this as any)[key] = parser(req[key]);
      if (isNaN(numValue)) {
        throw new ValidationError(`Invalid value for ${key}: ${req[key]}`);
      }
    }
  }

  private initDate(key: string, req: any): void {
    if (req[key] != null) {
      const numValue = (this as any)[key] = parseInt(req[key]);
      const dateValue = (this as any)[key] = new Date(isNaN(numValue) ? req[key] : numValue);
      if (isNaN(dateValue.getTime())) {
        throw new ValidationError(`Invalid value for ${key}: ${req[key]}`);
      }
    }
  }

  public toMap(db: Db): any {
    let queryMap: any = {};
    const where: any = {};

    if (this.ids?.length) {
      queryMap.where = {
        [db.locationTableColumns.id || 'id']: {[Op.in]: this.ids},
      };

      queryMap.order = [[db.locationTableColumns.timestamp || 'timestamp', this.order.toUpperCase()]];
      // If we have ids, we don't need any other filters
      return queryMap;
    }

    if (this.limit != null) {
      queryMap.limit = this.limit;
    }

    if (this.offset != null) {
      queryMap.offset = this.offset;
    }

    if (this.deviceId != null) {
      const deviceIds = this.deviceId.split(/\s*,\s*/);
      where[db.locationTableColumns.deviceId || 'deviceId'] = {[Op.in]: deviceIds};
    }

    const colMapping: any = db.locationTableColumns
    if (this.startDate != null || this.endDate != null) {
      const start = this.startDate == null ? 0 : this.startDate.getTime();
      const end = this.endDate == null ? new Date().getTime() : this.endDate.getTime();
      const column = colMapping.timestamp || 'timestamp';
      const where_t: any = where[column] = {};
      where_t[Op.between] = [start, end];
    }

    if (this.minId != null || this.maxId != null) {
      const column = colMapping.id || 'id';
      const where_id: any = where[column] = {};
      if (this.minId == null && this.maxId != null) {
        where_id[Op.lte] = this.maxId;
      } else if (this.minId != null && this.maxId == null) {
        where_id[Op.gte] = this.minId;
      } else {
        where_id[Op.between] = [this.minId, this.maxId];
      }
    }

    if (this.country != null) {
      where[colMapping.country || 'country'] = this.country;
    }

    if (this.locality != null) {
      where[colMapping.locality || 'locality'] = this.locality;
    }

    if (this.postalCode != null) {
      where[colMapping.postalCode || 'postalCode'] = this.postalCode;
    }

    if (this.description != null) {
      where[colMapping.description || 'description'] = {[Op.like]: `%${this.description}%`};
    }

    if (this.minLatitude != null || this.maxLatitude != null) {
      const column = colMapping.latitude || 'latitude';
      const where_lat: any = where[column] = {};
      if (this.minLatitude == null && this.maxLatitude != null) {
        where_lat[Op.lte] = this.maxLatitude;
      } else if (this.minLatitude != null && this.maxLatitude == null) {
        where_lat[Op.gte] = this.minLatitude;
      } else {
        where_lat[Op.between] = [this.minLatitude, this.maxLatitude];
      }
    }

    if (this.minLongitude != null || this.maxLongitude != null) {
      const column = colMapping.longitude || 'longitude';
      const where_lon: any = where[column] = {};
      if (this.minLongitude == null && this.maxLongitude != null) {
        where_lon[Op.lte] = this.maxLongitude;
      } else if (this.minLongitude != null && this.maxLongitude == null) {
        where_lon[Op.gte] = this.minLongitude;
      } else {
        where_lon[Op.between] = [this.minLongitude, this.maxLongitude];
      }
    }

    queryMap.where = where;
    queryMap.order = [[colMapping[this.orderBy], this.order.toUpperCase()]];
    return queryMap;
  }
}

export default LocationRequest;