package device

import (
	"errors"
	"fmt"
	"log"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/jinzhu/gorm"
	"gitlab.com/pactual1/backend/models"
	"gitlab.com/pactual1/backend/services/messaging"
	"gitlab.com/pactual1/backend/shared"
)

func GetDevicesForContract(contractID uint64, companyID int) ([]models.Device, int, error) {
	// Fetch the contract from the database
	var contract models.Contract
	if err := shared.GetDb().Where("id = ?", contractID).First(&contract).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			log.Printf("GetDevicesByContract Error: No contract found: %v", err)
			return nil, http.StatusNotFound, err
		} else {
			log.Printf("GetDevicesByContract Error: Database error: %v", err)
			return nil, http.StatusInternalServerError, err
		}
	}
	log.Printf("This is the device IDS ID: %v", contract.DeviceIDs)
	return GetDevicesByID(contract.DeviceIDs)
}

func GetDevicesByID(deviceIDs []int64) ([]models.Device, int, error) {
	// Create a slice to hold the devices
	var devices []models.Device

	// Convert the integer array to a comma-separated string
	idStrings := []string{}
	for _, id := range deviceIDs {
		idStrings = append(idStrings, strconv.FormatInt(id, 10))
	}
	idStr := strings.Join(idStrings, ",")

	// Construct the SQL query manually
	sqlQuery := fmt.Sprintf("SELECT * FROM devices WHERE id IN (%s)", idStr)

	// Fetch devices from the database based on DeviceIDs in the contract
	// Execute the query
	if err := shared.GetDb().Raw(sqlQuery).Scan(&devices).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			log.Printf("GetDevicesByContract Error: No devices found: %v", err)
			return devices, http.StatusNotFound, err
		} else {
			log.Printf("GetDevicesByContract Error: Database error: %v", err)
			return devices, http.StatusInternalServerError, err
		}
	}
	return devices, http.StatusOK, nil
}

func GetDevicesByImei(imeiIDs []string) ([]models.Device, int, error) {
	// Create a slice to hold the devices
	var devices []models.Device

	// Quote each IMEI ID
	for i, imei := range imeiIDs {
		imeiIDs[i] = "'" + imei + "'"
	}

	// Join the quoted IMEI IDs into a comma-separated string
	idStr := strings.Join(imeiIDs, ",")

	// Construct the SQL query manually
	sqlQuery := fmt.Sprintf("SELECT * FROM devices WHERE imei IN (%s)", idStr)

	// Fetch devices from the database based on DeviceIDs in the contract
	// Execute the query
	if err := shared.GetDb().Raw(sqlQuery).Scan(&devices).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			log.Printf("GetDevicesByContract Error: No devices found: %v", err)
			return devices, http.StatusNotFound, err
		} else {
			log.Printf("GetDevicesByContract Error: Database error: %v", err)
			return devices, http.StatusInternalServerError, err
		}
	}
	return devices, http.StatusOK, nil
}

func GetDeviceInfoForContract(deviceID uint64, contract models.Contract) (models.GeoJSONFeatureCollection, int, error) {

	var deviceInfos []models.DeviceInfo

	// Create a GeoJSON feature collection
	var featureCollection models.GeoJSONFeatureCollection
	featureCollection.Type = "FeatureCollection"
	featureCollection.Features = []models.GeoJSONFeature{}

	// Modify your query to include filtering based on the contract's creation date
	query := shared.GetDb().Where("device_id = ?", deviceID).Where("created_at >= ? AND created_at <= ?", contract.StartTime, contract.EndTime)

	if err := query.Find(&deviceInfos).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			log.Printf("GetDeviceData Error: No device info found: %v", err)
			return featureCollection, http.StatusNotFound, err
		} else {
			log.Printf("GetDeviceData Error: Database error: %v", err)
			return featureCollection, http.StatusNotFound, err
		}
	}

	deviceInfosResponse := models.ConvertDeviceInfoToResponse(deviceInfos)

	// Loop through each deviceInfo to create GeoJSON features
	for _, info := range deviceInfosResponse {
		info.RawJSON = ""
		feature := models.GeoJSONFeature{
			Type:               "Feature",
			DeviceInfoResponse: &info,
			Geometry: models.GeoJSONGeometry{
				Type:        "Point",
				Coordinates: []float64{info.Lon, info.Lat},
			},
			Properties: map[string]interface{}{},
		}
		featureCollection.Features = append(featureCollection.Features, feature)
	}

	return featureCollection, 0, nil

}

func SaveDeviceInfoToDB(deviceInfo models.DeviceInfo, rawData []byte) (models.DeviceInfo, models.Device, error) {
	deviceInfo.RawJSON = string(rawData)
	deviceInfo.X = deviceInfo.AccInfo.X
	deviceInfo.Y = deviceInfo.AccInfo.Y
	deviceInfo.Z = deviceInfo.AccInfo.Z

	var device models.Device
	if err := shared.GetDb().Unscoped().Where("device_id = ?", deviceInfo.ExternalDeviceID).First(&device).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			newDevice := models.Device{
				IMEI:                deviceInfo.IMEI,
				IMSI:                deviceInfo.IMSI,
				DeviceID:            deviceInfo.ExternalDeviceID,
				DeviceConfiguration: string(rawData),
			}
			if err := shared.GetDb().Create(&newDevice).Error; err != nil {
				return deviceInfo, device, fmt.Errorf("CREATE -Device DB Error: %v", err)
			}
			deviceInfo.DeviceID = newDevice.ID
		} else {
			return deviceInfo, device, fmt.Errorf("CREATE -Device DB Error: %v", err)
		}
	} else {
		if device.DeletedAt.Valid {
			if err := shared.GetDb().Exec("UPDATE devices SET deleted_at = NULL, company_id = NULL WHERE id = ?", device.ID).Error; err != nil {
				return deviceInfo, device, fmt.Errorf("UNDELETE -Device DB Error: %v", err)
			}
		}
		deviceInfo.DeviceID = device.ID
	}

	if err := shared.GetDb().Create(&deviceInfo).Error; err != nil {
		return deviceInfo, device, fmt.Errorf("SaveDeviceInfo CREATE -DeviceInfo DB Error: %v", err)
	}

	go func(deviceInfo models.DeviceInfo) {
		var contract models.Contract
		if err := shared.GetDb().Where("device_ids @> ARRAY[?]::integer[]", deviceInfo.DeviceID).First(&contract).Error; err != nil {
			log.Printf("could not find associated contract: %v", err)

		}

		messagingChannel := messaging.GetMessagingChannel()

		if deviceInfo.Temperature > contract.MaxTemp || deviceInfo.Temperature < contract.MinTemp {

			notification := models.Notification{
				Title:            "Temperature Alert",
				NotificationType: "Temperature",
				Text:             fmt.Sprintf("Device %s has a temperature outside the permitted range.", deviceInfo.ExternalDeviceID),
				CompanyID:        int(contract.BuyerID),
			}

			sellerNotification := models.Notification{
				Title:            "Temperature Alert",
				NotificationType: "Temperature",
				Text:             fmt.Sprintf("Device %s has a temperature outside the permitted range.", deviceInfo.ExternalDeviceID),
				CompanyID:        int(contract.SellerID),
			}

			messagingChannel <- notification
			messagingChannel <- sellerNotification
		}
	}(deviceInfo)

	return deviceInfo, device, nil
}

func CountDeviceInfoByCompany(companyID uint) (int64, error) {
	var contracts []models.Contract
	var allDeviceIDs []int64
	var count int64

	// Find all contracts where the company is either a buyer or a seller
	if err := shared.GetDb().Where("buyer_id = ? OR seller_id = ?", companyID, companyID).Find(&contracts).Error; err != nil {
		log.Printf("CountDeviceInfoByCompany Error: Database error: %v", err)
		return 0, err
	}

	// Aggregate all DeviceIDs from these contracts
	for _, contract := range contracts {
		allDeviceIDs = append(allDeviceIDs, contract.DeviceIDs...)
	}

	// Count DeviceInfo entries related to these DeviceIDs
	if err := shared.GetDb().Model(&models.DeviceInfo{}).Where("device_id IN (?)", allDeviceIDs).Count(&count).Error; err != nil {
		log.Printf("CountDeviceInfoByCompany Error: Database error: %v", err)
		return 0, err
	}

	return count, nil
}

func CountDeviceBreachedAndNormalDevicesByCompany(companyID uint, startTime, endTime time.Time) (int64, int64, map[string]map[string]int64, error) {
	var contracts []models.Contract
	var allDeviceIDs []int64
	var inRangeCount, outOfRangeCount int64
	monthlyCounts := make(map[string]map[string]int64)

	// Fetch all contracts related to the company
	if err := shared.GetDb().Where("buyer_id = ? OR seller_id = ?", companyID, companyID).Find(&contracts).Error; err != nil {
		log.Printf("CountDeviceInfoByCompany Error: Database error: %v", err)
		return 0, 0, nil, err
	}

	// Aggregate all DeviceIDs
	for _, contract := range contracts {
		allDeviceIDs = append(allDeviceIDs, contract.DeviceIDs...)
	}

	// Iterate through each month within the specified date range
	for dt := startTime; dt.Before(endTime); dt = dt.AddDate(0, 1, 0) {
		monthEnd := dt.AddDate(0, 1, 0)
		if monthEnd.After(endTime) {
			monthEnd = endTime
		}

		startUnix := dt.Unix()
		endUnix := monthEnd.Unix()

		// Initialize monthlyCounts for the current month
		monthKey := dt.Format("2006-01")
		monthlyCounts[monthKey] = map[string]int64{"inRange": 0, "outOfRange": 0}

		// Count DeviceInfo entries with temperatures in range and out of range for the current month
		for _, contract := range contracts {
			var inRange, outOfRange int64
			err := shared.GetDb().Model(&models.DeviceInfo{}).
				Where("device_id IN (?) AND temperature >= ? AND temperature <= ? AND timestamp >= ? AND timestamp <= ?",
					allDeviceIDs, contract.MinTemp, contract.MaxTemp, startUnix, endUnix).
				Count(&inRange).Error
			if err != nil {
				return 0, 0, nil, err
			}

			err = shared.GetDb().Model(&models.DeviceInfo{}).
				Where("device_id IN (?) AND (temperature < ? OR temperature > ?) AND timestamp >= ? AND timestamp <= ?",
					allDeviceIDs, contract.MinTemp, contract.MaxTemp, startUnix, endUnix).
				Count(&outOfRange).Error
			if err != nil {
				return 0, 0, nil, err
			}

			// Update the counts for the current month
			monthlyCounts[monthKey]["inRange"] += inRange
			monthlyCounts[monthKey]["outOfRange"] += outOfRange

			// Update the total counts
			inRangeCount += inRange
			outOfRangeCount += outOfRange
		}
	}

	return inRangeCount, outOfRangeCount, monthlyCounts, nil
}

type ContractLocationMatch struct {
	ContractID   uint
	DeviceInfoID uint
}

func FetchMatchingContractsAndDeviceInfo(companyID uint64, startTime, endTime time.Time) ([]ContractLocationMatch, error) {
	var contracts []models.Contract
	var results []ContractLocationMatch
	registeredDevices := make(map[uint]bool) // Map to keep track of registered devices

	//Fetch contracts
	err := shared.GetDb().
		Where("start_time >= ? AND end_time <= ? AND (buyer_id = ? OR seller_id = ?) ", startTime, endTime, companyID, companyID).
		Find(&contracts).Error

	if err != nil {
		return nil, err
	}
	//Loop through each contract to find matching DeviceInfo
	for _, contract := range contracts {
		var deviceInfos []models.DeviceInfo

		deviceIDStr := strings.Trim(strings.Join(strings.Fields(fmt.Sprint(contract.DeviceIDs)), ","), "[]")
		queryString := fmt.Sprintf("device_id IN (%s) AND created_at >= ? AND created_at <= ?", deviceIDStr)

		err := shared.GetDb().
			Where(queryString, contract.StartTime, contract.EndTime).
			Find(&deviceInfos).Error

		if err != nil {
			return nil, err
		}

		// Compare locations and created_at
		for _, deviceInfo := range deviceInfos {
			// Continue to the next iteration if this device has already been registered
			if registeredDevices[deviceInfo.DeviceID] {
				continue
			}

			if shared.DistanceKm(contract.StartLat, contract.StartLon, deviceInfo.Lat, deviceInfo.Lon) <= 1 ||
				shared.DistanceKm(contract.EndLat, contract.EndLon, deviceInfo.Lat, deviceInfo.Lon) <= 1 {

				results = append(results, ContractLocationMatch{
					ContractID:   contract.ID,
					DeviceInfoID: deviceInfo.ID,
				})
				registeredDevices[deviceInfo.DeviceID] = true // Mark this device as registered
			}
		}
	}

	return results, nil
}
