package qc

import (
	"encoding/json"
	"fmt"
	"net/http"
	"verofy-backend/models"

	"github.com/gin-gonic/gin"
	"gorm.io/gorm"
)

type UndergroundEndpointResult struct {
	SegmentID     int                    `json:"segment_id"`
	SegmentName   string                 `json:"segment_name"`
	Type          string                 `json:"type"`
	IsValid       bool                   `json:"is_valid"`
	ErrorMessage  string                 `json:"error_message,omitempty"`
	StartEndpoint string                 `json:"start_endpoint,omitempty"`
	EndEndpoint   string                 `json:"end_endpoint,omitempty"`
	Geometry      map[string]interface{} `json:"geometry,omitempty"`
}

type UndergroundEndpointSummary struct {
	TotalUndergroundSegments int                         `json:"total_underground_segments"`
	ValidSegments            int                         `json:"valid_segments"`
	InvalidSegments          int                         `json:"invalid_segments"`
	PassRate                 float64                     `json:"pass_rate"`
	Results                  []UndergroundEndpointResult `json:"results"`
}

func UndergroundEndpointsRoute(router *gin.Engine, db *gorm.DB, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol string) {
	// Full underground endpoints summary endpoint
	router.GET("/api/qc/underground-endpoints", func(c *gin.Context) {
		mapID := c.Query("map_id")
		zone := c.Query("zone")
		if mapID == "" || zone == "" {
			c.JSON(http.StatusBadRequest, gin.H{"error": "map_id and zone are required"})
			return
		}

		summary, err := CheckUndergroundEndpoints(db, mapID, zone, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}

		c.JSON(http.StatusOK, summary)
	})

	// Invalid segments only endpoint
	router.GET("/api/qc/underground-endpoints/invalid", func(c *gin.Context) {
		mapID := c.Query("map_id")
		zone := c.Query("zone")
		if mapID == "" || zone == "" {
			c.JSON(http.StatusBadRequest, gin.H{"error": "map_id and zone are required"})
			return
		}

		invalid, err := GetInvalidUndergroundEndpoints(db, mapID, zone, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}

		c.JSON(http.StatusOK, gin.H{
			"invalid_segments": invalid,
			"count":            len(invalid),
		})
	})

	// Update QC flags endpoint
	router.POST("/api/qc/underground-endpoints/update-flags", func(c *gin.Context) {
		var request struct {
			SegmentIDs []int  `json:"segment_ids"`
			MapID      string `json:"map_id"`
			Zone       string `json:"zone"`
		}

		if err := c.ShouldBindJSON(&request); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		err := UpdateUndergroundEndpointFlags(db, request.SegmentIDs, schema, segmentTable, idCol, qcFlagCol)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}

		c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("Updated QC flags for %d segments", len(request.SegmentIDs))})
	})
}

// CheckUndergroundEndpoints validates that underground segments have poles or access points at both endpoints
func CheckUndergroundEndpoints(db *gorm.DB, mapID, zone, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol string) (*UndergroundEndpointSummary, error) {
	var segments []models.SegmentGeoJSON
	table := fmt.Sprintf("%s.%s", schema, segmentTable)

	// Query underground segments
	err := db.Table(table).
		Select(fmt.Sprintf("id_0, %s, segment_type, segment_status, %s, protection_status, %s, ST_AsGeoJSON(geom)::json AS geometry", mapIDCol, idCol, qcFlagCol)).
		Where(fmt.Sprintf("%s = ? AND %s = ? AND LOWER(segment_type) = ?", mapIDCol, zoneCol), mapID, zone, "underground").
		Find(&segments).Error

	if err != nil {
		return nil, fmt.Errorf("failed to fetch underground segments: %w", err)
	}

	// Get poles and access points for the same map/zone
	poles, err := getPoles(db, mapID, schema)
	if err != nil {
		return nil, fmt.Errorf("failed to fetch poles: %w", err)
	}

	accessPoints, err := getAccessPoints(db, mapID, schema)
	if err != nil {
		return nil, fmt.Errorf("failed to fetch access points: %w", err)
	}

	summary := &UndergroundEndpointSummary{
		TotalUndergroundSegments: len(segments),
		Results:                  make([]UndergroundEndpointResult, 0, len(segments)),
	}

	for _, segment := range segments {
		result := validateUndergroundEndpoints(segment, poles, accessPoints)
		summary.Results = append(summary.Results, result)

		if result.IsValid {
			summary.ValidSegments++
		} else {
			summary.InvalidSegments++
		}
	}

	// Calculate pass rate
	if summary.TotalUndergroundSegments > 0 {
		summary.PassRate = float64(summary.ValidSegments) / float64(summary.TotalUndergroundSegments) * 100
	}

	return summary, nil
}

// validateUndergroundEndpoints checks if underground segment has poles/access points at both ends
func validateUndergroundEndpoints(segment models.SegmentGeoJSON, poles []models.PolesGeoJSON, accessPoints []models.AccessPointGeoJSON) UndergroundEndpointResult {
	result := UndergroundEndpointResult{
		SegmentID: int(segment.ID),
		Type:      segment.SegmentType,
		IsValid:   false,
	}

	// Parse the geometry to get start and end coordinates
	if len(segment.Geometry) == 0 {
		result.ErrorMessage = "Segment has no geometry data"
		return result
	}

	startCoord, endCoord, geometry, err := getSegmentEndpoints(segment.Geometry)
	if err != nil {
		result.ErrorMessage = fmt.Sprintf("Failed to parse geometry: %v", err)
		return result
	}

	result.Geometry = geometry

	// Check for poles/access points near start and end coordinates
	// Using a buffer distance of ~10 meters (0.0001 degrees approximately)
	bufferDistance := 0.0001

	startHasEndpoint := hasEndpointNearCoordinate(startCoord, poles, accessPoints, bufferDistance)
	endHasEndpoint := hasEndpointNearCoordinate(endCoord, poles, accessPoints, bufferDistance)

	startEndpointType := getEndpointTypeNearCoordinate(startCoord, poles, accessPoints, bufferDistance)
	endEndpointType := getEndpointTypeNearCoordinate(endCoord, poles, accessPoints, bufferDistance)

	result.StartEndpoint = startEndpointType
	result.EndEndpoint = endEndpointType

	if startHasEndpoint && endHasEndpoint {
		result.IsValid = true
	} else {
		errorParts := []string{}
		if !startHasEndpoint {
			errorParts = append(errorParts, "no pole/access point at start")
		}
		if !endHasEndpoint {
			errorParts = append(errorParts, "no pole/access point at end")
		}
		result.ErrorMessage = fmt.Sprintf("Underground segment missing endpoints: %s", fmt.Sprintf("%s", errorParts))
	}

	return result
}

// getSegmentEndpoints extracts start and end coordinates from segment geometry
func getSegmentEndpoints(geometryRaw json.RawMessage) ([2]float64, [2]float64, map[string]interface{}, error) {
	var geometry map[string]interface{}

	if err := json.Unmarshal(geometryRaw, &geometry); err != nil {
		return [2]float64{}, [2]float64{}, nil, fmt.Errorf("invalid GeoJSON: %w", err)
	}

	geometryType, ok := geometry["type"].(string)
	if !ok {
		return [2]float64{}, [2]float64{}, geometry, fmt.Errorf("missing or invalid geometry type")
	}

	coordinates, ok := geometry["coordinates"]
	if !ok {
		return [2]float64{}, [2]float64{}, geometry, fmt.Errorf("missing coordinates")
	}

	var startCoord, endCoord [2]float64

	switch geometryType {
	case "LineString":
		// LineString coordinates are [[x,y], [x,y], ...]
		if coordArray, ok := coordinates.([]interface{}); ok && len(coordArray) >= 2 {
			if startPoint, ok := coordArray[0].([]interface{}); ok && len(startPoint) >= 2 {
				if x, ok := startPoint[0].(float64); ok {
					startCoord[0] = x
				}
				if y, ok := startPoint[1].(float64); ok {
					startCoord[1] = y
				}
			}
			if endPoint, ok := coordArray[len(coordArray)-1].([]interface{}); ok && len(endPoint) >= 2 {
				if x, ok := endPoint[0].(float64); ok {
					endCoord[0] = x
				}
				if y, ok := endPoint[1].(float64); ok {
					endCoord[1] = y
				}
			}
		} else {
			return [2]float64{}, [2]float64{}, geometry, fmt.Errorf("invalid LineString coordinates")
		}
	case "MultiLineString":
		// For MultiLineString, use first and last coordinates of the entire geometry
		if coordArrays, ok := coordinates.([]interface{}); ok && len(coordArrays) > 0 {
			// Get start from first LineString
			if firstLine, ok := coordArrays[0].([]interface{}); ok && len(firstLine) >= 2 {
				if startPoint, ok := firstLine[0].([]interface{}); ok && len(startPoint) >= 2 {
					if x, ok := startPoint[0].(float64); ok {
						startCoord[0] = x
					}
					if y, ok := startPoint[1].(float64); ok {
						startCoord[1] = y
					}
				}
			}
			// Get end from last LineString
			if lastLine, ok := coordArrays[len(coordArrays)-1].([]interface{}); ok && len(lastLine) >= 2 {
				if endPoint, ok := lastLine[len(lastLine)-1].([]interface{}); ok && len(endPoint) >= 2 {
					if x, ok := endPoint[0].(float64); ok {
						endCoord[0] = x
					}
					if y, ok := endPoint[1].(float64); ok {
						endCoord[1] = y
					}
				}
			}
		} else {
			return [2]float64{}, [2]float64{}, geometry, fmt.Errorf("invalid MultiLineString coordinates")
		}
	default:
		return [2]float64{}, [2]float64{}, geometry, fmt.Errorf("unsupported geometry type: %s", geometryType)
	}

	return startCoord, endCoord, geometry, nil
}

// hasEndpointNearCoordinate checks if there's a pole or access point within buffer distance of coordinate
func hasEndpointNearCoordinate(coord [2]float64, poles []models.PolesGeoJSON, accessPoints []models.AccessPointGeoJSON, buffer float64) bool {
	// Check poles
	for _, pole := range poles {
		if poleCoord, err := getPointCoordinates(pole.Geometry); err == nil {
			if distance(coord, poleCoord) <= buffer {
				return true
			}
		}
	}

	// Check access points
	for _, ap := range accessPoints {
		if apCoord, err := getPointCoordinates(ap.Geometry); err == nil {
			if distance(coord, apCoord) <= buffer {
				return true
			}
		}
	}

	return false
}

// getEndpointTypeNearCoordinate returns the type of endpoint near the coordinate
func getEndpointTypeNearCoordinate(coord [2]float64, poles []models.PolesGeoJSON, accessPoints []models.AccessPointGeoJSON, buffer float64) string {
	// Check poles first
	for _, pole := range poles {
		if poleCoord, err := getPointCoordinates(pole.Geometry); err == nil {
			if distance(coord, poleCoord) <= buffer {
				return fmt.Sprintf("Pole (ID: %d)", *pole.ID)
			}
		}
	}

	// Check access points
	for _, ap := range accessPoints {
		if apCoord, err := getPointCoordinates(ap.Geometry); err == nil {
			if distance(coord, apCoord) <= buffer {
				return fmt.Sprintf("Access Point (ID: %d)", *ap.ID)
			}
		}
	}

	return "None"
}

// getPointCoordinates extracts coordinates from point geometry
func getPointCoordinates(geometryRaw json.RawMessage) ([2]float64, error) {
	var geometry map[string]interface{}

	if err := json.Unmarshal(geometryRaw, &geometry); err != nil {
		return [2]float64{}, fmt.Errorf("invalid GeoJSON: %w", err)
	}

	geometryType, ok := geometry["type"].(string)
	if !ok || geometryType != "Point" {
		return [2]float64{}, fmt.Errorf("not a Point geometry")
	}

	coordinates, ok := geometry["coordinates"].([]interface{})
	if !ok || len(coordinates) < 2 {
		return [2]float64{}, fmt.Errorf("invalid Point coordinates")
	}

	var coord [2]float64
	if x, ok := coordinates[0].(float64); ok {
		coord[0] = x
	}
	if y, ok := coordinates[1].(float64); ok {
		coord[1] = y
	}

	return coord, nil
}

// distance calculates simple Euclidean distance between two coordinates
func distance(a, b [2]float64) float64 {
	dx := a[0] - b[0]
	dy := a[1] - b[1]
	return dx*dx + dy*dy // Using squared distance for efficiency
}

// getPoles fetches poles for the given map ID
func getPoles(db *gorm.DB, mapID, schema string) ([]models.PolesGeoJSON, error) {
	var poles []models.PolesGeoJSON
	table := fmt.Sprintf("%s.poles", schema)

	err := db.Table(table).
		Select("gid, id, mapprojectid, name, tags, group1, group2, owner, poleheight, attachmentheight, ST_AsGeoJSON(geom)::json AS geometry").
		Where("mapprojectid = ?", mapID).
		Find(&poles).Error

	return poles, err
}

// getAccessPoints fetches access points for the given map ID
func getAccessPoints(db *gorm.DB, mapID, schema string) ([]models.AccessPointGeoJSON, error) {
	var accessPoints []models.AccessPointGeoJSON
	table := fmt.Sprintf("%s.access_points", schema)

	err := db.Table(table).
		Select(`gid, id, name, mapprojectid, latitude, longitude, manufacturer, size, locked, description, aka,
			createdby, createddate, modifiedby, modifieddate, historyid, group1, group2, typeid, statusid,
			crmvendorid, billdate, ST_AsGeoJSON(geom)::json AS geometry`).
		Where("mapprojectid = ?", mapID).
		Find(&accessPoints).Error

	return accessPoints, err
}

// GetInvalidUndergroundEndpoints returns only the segments that failed the check
func GetInvalidUndergroundEndpoints(db *gorm.DB, mapID, zone, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol string) ([]UndergroundEndpointResult, error) {
	summary, err := CheckUndergroundEndpoints(db, mapID, zone, schema, segmentTable, mapIDCol, zoneCol, idCol, qcFlagCol)
	if err != nil {
		return nil, err
	}

	var invalid []UndergroundEndpointResult
	for _, result := range summary.Results {
		if !result.IsValid {
			invalid = append(invalid, result)
		}
	}

	return invalid, nil
}

// UpdateUndergroundEndpointFlags updates QC flags for invalid segments
func UpdateUndergroundEndpointFlags(db *gorm.DB, segmentIDs []int, schema, segmentTable, idCol, qcFlagCol string) error {
	if len(segmentIDs) == 0 {
		return nil
	}

	table := fmt.Sprintf("%s.%s", schema, segmentTable)

	return db.Table(table).
		Where(fmt.Sprintf("%s IN ?", idCol), segmentIDs).
		Update(qcFlagCol, "underground_endpoint_issue").Error
}
