import { Injectable, Logger, InternalServerErrorException, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { IpEntity } from './entities/ip.entity';
import { GeoName } from '../csv-import/entities/geoname.entity';
import { CityBlockIpv4, CityBlockIpv6 } from '../csv-import/entities/city.entity';
import { IpResponseDto } from './dto/ip.response.dto';
import * as ipaddr from 'ipaddr.js';
import { CreateIpDto } from './dto/create-ip.sto';
import { AsnEntity } from '../csv-import/entities/asn.entity';

@Injectable()
export class IpService {
  private readonly logger = new Logger(IpService.name);

  constructor(
    @InjectRepository(IpEntity) private ipRepository: Repository<IpEntity>,
    @InjectRepository(GeoName) private geoNameRepository: Repository<GeoName>,
    @InjectRepository(CityBlockIpv4) private cityBlockIpv4Repository: Repository<CityBlockIpv4>,
    @InjectRepository(CityBlockIpv6) private cityBlockIpv6Repository: Repository<CityBlockIpv6>,
    @InjectRepository(AsnEntity) private asnRepository: Repository<AsnEntity>,
  ) {}

  // Check if the IP address is IPv4
  // This method uses the ipaddr.js library to parse the IP address and determine its version.
  private isIpv4(ip: string): boolean {
    try {
      this.logger.log(`Determining IP version for IP: ${ip}`);
      const addr = ipaddr.parse(ip);
      return addr.kind() === 'ipv4';
    } catch (error) {
      this.logger.error(`Error determining IP version: ${ip}`, error.stack);
      throw new NotFoundException(`Invalid IP address: ${ip}`);
    }
  }

  private ipToNumber(ip: string): number {
    return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet), 0);
  }

  // Find matching CIDR block for IPv4
  private async findMatchingCidrIpv4(ip: string): Promise<CityBlockIpv4 | null> {
    this.logger.log(`Finding matching CIDR for IPv4: ${ip}`);

    try {
      // Use INET_ATON to convert IPv4 address to a numeric value for comparison
      // The query checks if the numeric value of the IP address falls within the range defined by the network and prefix length.
      const numericIp = this.ipToNumber(ip);
      const query = `
        SELECT * FROM city_block_ipv4
        WHERE ? BETWEEN network AND end_ip
        LIMIT 1;
      `;

      const cityBlock = await this.cityBlockIpv4Repository.query(query, [numericIp]);

      if (!cityBlock || cityBlock.length === 0) {
        this.logger.warn(`No matching CIDR block found for IP: ${ip}`);
        return null;
      }

      return cityBlock[0];
    } catch (error) {
      this.logger.error(`Error finding matching CIDR for IPv4: ${ip}`, error.stack);
      throw new InternalServerErrorException(`Error finding matching CIDR for IPv4: ${ip}`);
    }
  }

  private async findMatchingCidrIpv6(ip: string): Promise<CityBlockIpv6 | null> {
    this.logger.log(`Finding matching CIDR for IPv6: ${ip}`);

    try {
      // Use INET6_ATON to convert IPv6 address to a numeric value for comparison
      // The query checks if the numeric value of the IP address falls within the range defined by the network and prefix length.
      const query = `
      SELECT * FROM city_block_ipv6
      WHERE INET6_ATON(?) BETWEEN network AND end_ip
      LIMIT 1;
    `;

      const cityBlock = await this.cityBlockIpv6Repository.query(query, [ip]);

      if (!cityBlock) {
        this.logger.warn(`No matching CIDR block found for IP: ${ip}`);
        return null;
      }

      return cityBlock[0] || null;
    } catch (error) {
      this.logger.error(`Error finding matching CIDR for IPv6: ${ip}`, error.stack);
      throw new InternalServerErrorException(`Error finding matching CIDR for IPv6: ${ip}`);
    }
  }

  private convertIpToNumeric(parsedIp: ipaddr.IPv4 | ipaddr.IPv6): bigint {
    const bytes = parsedIp.toByteArray();
    return bytes.reduce((acc, byte) => (acc << 8n) + BigInt(byte), 0n);
  }

  private async findAsnDetails(ip: string): Promise<AsnEntity | null> {
    this.logger.log(`Finding ASN details for IP: ${ip}`);

    try {
      const parsedIp = ipaddr.parse(ip);

      if (parsedIp.kind() === 'ipv4') {
        const ipNum = this.convertIpToNumeric(parsedIp);
        return await this.asnRepository
          .createQueryBuilder('asn')
          .where(':ipNum BETWEEN asn.startIpNum AND asn.endIpNum', { ipNum: ipNum.toString() })
          .getOne();
      } else {
        const ipBin = Buffer.from(parsedIp.toByteArray());
        return await this.asnRepository.createQueryBuilder('asn').where(':ipBin >= asn.startIpBin AND :ipBin <= asn.endIpBin', { ipBin }).getOne();
      }
    } catch (error) {
      this.logger.error(`Error finding ASN details for IP: ${ip}`, error.stack);
      throw new InternalServerErrorException(`Failed to find ASN details for IP: ${ip}`);
    }
  }

  private freeTierResponse(ip: string, userAgent: string, geoName?: GeoName, cityBlock?: any, asnDetails?: AsnEntity | null): IpResponseDto {
    this.logger.log(`Generating free tier response for IP: ${ip}`);

    return {
      ip: ip,
      user_agent: userAgent || null,
      geo_name: geoName
        ? {
            country_name: null,
            city_name: geoName.cityName || null,
            continent_code: geoName.continentCode || null,
            time_zone: geoName.timeZone || null,
            continent_name: geoName.continentName || null,
            country_iso_code: null,
            locale_code: geoName.localeCode || null,
            subdivision_1_iso_code: null,
            subdivision_1_name: null,
          }
        : null,
      city_block: cityBlock
        ? {
            accuracyRadius: cityBlock.accuracy_radius,
            latitude: cityBlock.latitude,
            longitude: cityBlock.longitude,
            network: Buffer.isBuffer(cityBlock.network) ? cityBlock.network.toString('hex') : cityBlock.network,
            postalCode: cityBlock.postal_code,
          }
        : null,
      asn: asnDetails
        ? {
            asn: asnDetails.asn,
            name: null,
            domain: null,
            endIp: null,
            startIp: null,
          }
        : null,
    };
  }

  private premiumTierResponse(ip: string, userAgent: string, cityBlock?: any, geoName?: GeoName, asnDetails?: AsnEntity | null): IpResponseDto {
    this.logger.debug(`Generating premium tier response for IP: ${ip}`);

    return {
      ip: ip,
      user_agent: userAgent || null,
      geo_name: geoName
        ? {
            city_name: geoName.cityName,
            country_name: geoName.countryName,
            continent_code: geoName.continentCode,
            time_zone: geoName.timeZone,
            continent_name: geoName.continentName,
            country_iso_code: geoName.countryIsoCode,
            locale_code: geoName.localeCode,
            subdivision_1_iso_code: geoName.subdivision1IsoCode,
            subdivision_1_name: geoName.subdivision1Name,
          }
        : null,
      city_block: cityBlock
        ? {
            accuracyRadius: cityBlock.accuracy_radius,
            latitude: cityBlock.latitude,
            longitude: cityBlock.longitude,
            network: Buffer.isBuffer(cityBlock.network) ? cityBlock.network.toString('hex') : cityBlock.network,
            postalCode: cityBlock.postal_code,
          }
        : null,
      asn: asnDetails
        ? {
            asn: asnDetails.asn,
            name: asnDetails.name,
            domain: asnDetails.domain,
            endIp: asnDetails.endIp,
            startIp: asnDetails.startIp,
          }
        : null,
    };
  }

  async getIpDetails(ip: string, tier: string, userAgent: string): Promise<IpResponseDto> {
    this.logger.log(`Fetching IP details for IP: ${ip} with tier: ${tier}`);

    // ip = '2001:df7:be80:432c:68ab:948:f7c2:3731';

    try {
      // Determine IP version IPv4 or IPv6
      const isIpv4Address = this.isIpv4(ip);

      let asnDetails: AsnEntity | null = null;
      let cityBlock: any;

      if (isIpv4Address) {
        cityBlock = await this.findMatchingCidrIpv4(ip);
      } else {
        cityBlock = await this.findMatchingCidrIpv6(ip);
      }

      // Find ASN details based on IP version
      asnDetails = await this.findAsnDetails(ip);

      // For free tier, return limited information
      if (tier === 'free') {
        this.logger.log(`Free tier requested for IP: ${ip}`);
        return this.freeTierResponse(ip, userAgent, null, cityBlock, asnDetails);
      }

      // For premium tier, continue with full data
      if (!cityBlock) {
        this.logger.warn(`No matching city block found for IP: ${ip}`);
        return this.premiumTierResponse(ip, userAgent, null, null, asnDetails);
      }

      // Find GeoName details for the IP
      const geonameId = cityBlock.geoname_id;

      this.logger.log(`Fetching GeoName for GeoName ID: ${geonameId}`);
      const geoName = await this.geoNameRepository.findOne({
        where: { geonameId: geonameId },
      });

      if (!geonameId || !geoName) {
        this.logger.warn(`No GeoName ID found for IP: ${ip}`);
        return this.premiumTierResponse(ip, userAgent, cityBlock, null, asnDetails);
      }

      await this.saveIpDetails({
        ip,
        city: geoName.cityName,
        country: geoName.countryName,
        userAgent: userAgent || null,
        asn: asnDetails ? asnDetails.asn : null,
      });

      // Return complete premium data
      return this.premiumTierResponse(ip, userAgent, cityBlock, geoName, asnDetails);
    } catch (error) {
      this.logger.error(`Error fetching IP details for IP: ${ip}`, error.stack);
      throw new InternalServerErrorException(`Error fetching IP details for IP: ${ip}`);
    }
  }

  async saveIpDetails(ipData: CreateIpDto): Promise<void> {
    const { ip, city, country, userAgent, asn } = ipData;

    try {
      const newIp = this.ipRepository.create({
        ip,
        city,
        country,
        userAgent,
        asn,
      });

      await this.ipRepository.save(newIp);
      this.logger.log(`IP details saved for IP: ${ip}`);
    } catch (error) {
      this.logger.error(`Error saving IP details for IP: ${ip}`, error.stack);
      throw new InternalServerErrorException(`Error saving IP details for IP: ${ip}`);
    }
  }
}
