package services

import (
	"context"
	"history-api/internal/dtos/request"
	"history-api/internal/dtos/response"
	"history-api/internal/gen/sqlc"
	"history-api/internal/models"
	"history-api/internal/repositories"
	"history-api/pkg/convert"

	"github.com/gofiber/fiber/v3"
	"github.com/jackc/pgx/v5/pgtype"
)

type GeometryService interface {
	GetGeometryByID(ctx context.Context, id string) (*response.GeometryResponse, *fiber.Error)
	SearchGeometries(ctx context.Context, req *request.SearchGeometryDto) ([]*response.GeometryResponse, *fiber.Error)
	SearchGeometriesByEntityName(ctx context.Context, req *request.SearchGeometriesByEntityNameDto) (*response.SearchGeometriesByEntityNameResponse, *fiber.Error)
	GetGeometriesByBoundWith(ctx context.Context, boundWith string) ([]*response.GeometryResponse, *fiber.Error)
}

type geometryService struct {
	geometryRepo repositories.GeometryRepository
}

func NewGeometryService(geometryRepo repositories.GeometryRepository) GeometryService {
	return &geometryService{
		geometryRepo: geometryRepo,
	}
}

func (s *geometryService) GetGeometryByID(ctx context.Context, id string) (*response.GeometryResponse, *fiber.Error) {
	geometryId, err := convert.StringToUUID(id)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid geometry ID format")
	}
	geometry, err := s.geometryRepo.GetByID(ctx, geometryId)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusNotFound, "Geometry not found")
	}

	return geometry.ToResponse(), nil
}

func (s *geometryService) GetGeometriesByBoundWith(ctx context.Context, boundWith string) ([]*response.GeometryResponse, *fiber.Error) {
	boundWithUUID, err := convert.StringToUUID(boundWith)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid bound_with geometry ID format")
	}
	geometries, err := s.geometryRepo.GetGeometriesByBoundWith(ctx, boundWithUUID)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch geometries by bound_with")
	}
	return models.GeometriesEntityToResponse(geometries), nil
}

func (s *geometryService) SearchGeometries(ctx context.Context, req *request.SearchGeometryDto) ([]*response.GeometryResponse, *fiber.Error) {
	params := sqlc.SearchGeometriesParams{}

	if req.MinLng != nil && req.MinLat != nil && req.MaxLng != nil && req.MaxLat != nil {
		if *req.MaxLng < *req.MinLng || *req.MaxLat < *req.MinLat {
			return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid bounding box coordinates")
		}
		params.SearchMinLng = pgtype.Float8{Float64: *req.MinLng, Valid: true}
		params.SearchMinLat = pgtype.Float8{Float64: *req.MinLat, Valid: true}
		params.SearchMaxLng = pgtype.Float8{Float64: *req.MaxLng, Valid: true}
		params.SearchMaxLat = pgtype.Float8{Float64: *req.MaxLat, Valid: true}
	} else {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Bounding box coordinates are required")
	}

	if req.TimePoint != nil {
		params.TimePoint = pgtype.Int4{Int32: *req.TimePoint, Valid: true}
	}

	if req.TimeRange != nil {
		params.TimeRange = pgtype.Int4{Int32: *req.TimeRange, Valid: true}
	}

	if req.EntityID != nil {
		entityId, err := convert.StringToUUID(*req.EntityID)
		if err != nil {
			return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid entity ID format")
		}
		params.EntityID = entityId
	}

	if req.ProjectID != nil {
		projectId, err := convert.StringToUUID(*req.ProjectID)
		if err != nil {
			return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project ID format")
		}
		params.ProjectID = projectId
	}

	if req.HasBound != nil {
		params.HasBound = pgtype.Bool{Bool: *req.HasBound, Valid: true}
	}

	geometries, err := s.geometryRepo.Search(ctx, params)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to search geometries")
	}

	return models.GeometriesEntityToResponse(geometries), nil
}

func (s *geometryService) SearchGeometriesByEntityName(
	ctx context.Context,
	req *request.SearchGeometriesByEntityNameDto,
) (*response.SearchGeometriesByEntityNameResponse, *fiber.Error) {
	limit := int32(req.Limit)
	if limit <= 0 {
		limit = 20
	}

	var cursorID pgtype.UUID
	if req.Cursor != "" {
		id, err := convert.StringToUUID(req.Cursor)
		if err != nil {
			return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid cursor format")
		}
		cursorID = id
	}

	rows, err := s.geometryRepo.SearchByEntityName(ctx, sqlc.SearchGeometriesByEntityNameParams{
		Name:       convert.StringToText(req.Name),
		CursorID:   cursorID,
		LimitCount: limit,
	})
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to search geometries by entity name")
	}

	byEntity := make(map[string]*response.EntityGeometriesSearchItem)
	order := make([]string, 0)

	for _, row := range rows {
		item := byEntity[row.EntityID]
		if item == nil {
			item = &response.EntityGeometriesSearchItem{
				EntityID:    row.EntityID,
				Name:        row.EntityName,
				Description: row.EntityDescription,
				Geometries:  make([]*response.EntityGeometrySearchGeo, 0),
			}
			byEntity[row.EntityID] = item
			order = append(order, row.EntityID)
		}

		if row.GeometryID == "" || len(row.DrawGeometry) == 0 {
			continue
		}

		item.Geometries = append(item.Geometries, &response.EntityGeometrySearchGeo{
			ID:           row.GeometryID,
			GeoType:      row.GeoType,
			DrawGeometry: row.DrawGeometry,
			BoundWith:    row.BoundWith,
			TimeStart:    row.TimeStart,
			TimeEnd:      row.TimeEnd,
		})
	}

	items := make([]*response.EntityGeometriesSearchItem, 0, len(order))
	for _, entityID := range order {
		items = append(items, byEntity[entityID])
	}

	nextCursor := ""
	if len(order) > 0 {
		nextCursor = order[len(order)-1]
	}

	return &response.SearchGeometriesByEntityNameResponse{
		Items:      items,
		NextCursor: nextCursor,
	}, nil
}
