package service

import (
	"context"
	"time"

	"github.com/RafayLabs/rcloud-base/internal/dao"
	"github.com/RafayLabs/rcloud-base/internal/models"
	commonv3 "github.com/RafayLabs/rcloud-base/proto/types/commonpb/v3"
	infrav3 "github.com/RafayLabs/rcloud-base/proto/types/infrapb/v3"
	"github.com/google/uuid"
	bun "github.com/uptrace/bun"
	"google.golang.org/protobuf/types/known/timestamppb"
)

// MetroService is the interface for metro operations
type MetroService interface {
	// create metro
	Create(ctx context.Context, metro *infrav3.Location) (*infrav3.Location, error)
	// get metro by id
	GetById(ctx context.Context, id uuid.UUID) (*infrav3.Location, error)
	// get metro by name
	GetByName(ctx context.Context, name string) (*infrav3.Location, error)
	// get metro id by name
	GetIDByName(ctx context.Context, name string) (uuid.UUID, error)
	// create or update metro
	Update(ctx context.Context, metro *infrav3.Location) (*infrav3.Location, error)
	// delete metro
	Delete(ctx context.Context, metro *infrav3.Location) (*infrav3.Location, error)
	// list metro
	List(ctx context.Context, partner string) (*infrav3.LocationList, error)
}

// metroService implements MetroService
type metroService struct {
	db *bun.DB
}

// NewProjectService return new project service
func NewMetroService(db *bun.DB) MetroService {
	return &metroService{db}
}

func (s *metroService) Create(ctx context.Context, metro *infrav3.Location) (*infrav3.Location, error) {

	var part models.Partner
	_, err := dao.GetByName(ctx, s.db, metro.Metadata.Partner, &part)
	if err != nil {
		return nil, err
	}

	//convert v3 spec to internal models
	metrodb := models.Metro{
		Name:           metro.Spec.Name,
		CreatedAt:      time.Now(),
		ModifiedAt:     time.Now(),
		Trash:          false,
		Latitude:       metro.Spec.Latitude,
		Longitude:      metro.Spec.Longitude,
		City:           metro.Spec.City,
		State:          metro.Spec.State,
		Country:        metro.Spec.Country,
		CountryCode:    metro.Spec.CountryCode,
		StateCode:      metro.Spec.StateCode,
		OrganizationId: uuid.Nil,
		PartnerId:      part.ID,
	}
	_, err = dao.Create(ctx, s.db, &metrodb)
	if err != nil {
		return nil, err
	}

	return metro, nil

}

func (s *metroService) GetByName(ctx context.Context, name string) (*infrav3.Location, error) {

	var metro infrav3.Location

	entity, err := dao.GetByName(ctx, s.db, name, &models.Metro{})
	if err != nil {
		return nil, err
	}

	if metrodb, ok := entity.(*models.Metro); ok {
		location := &infrav3.Location{
			Metadata: &commonv3.Metadata{
				Name:       metrodb.Name,
				ModifiedAt: timestamppb.New(metrodb.ModifiedAt),
			},
			Spec: &infrav3.Metro{
				Name:        metrodb.Name,
				Country:     metrodb.Country,
				City:        metrodb.City,
				State:       metrodb.State,
				Latitude:    metrodb.Latitude,
				Longitude:   metrodb.Longitude,
				StateCode:   metrodb.StateCode,
				CountryCode: metrodb.CountryCode,
			},
		}
		return location, nil

	}
	return &metro, nil
}

func (s *metroService) GetById(ctx context.Context, id uuid.UUID) (*infrav3.Location, error) {
	var location infrav3.Location

	entity, err := dao.GetByID(ctx, s.db, id, &models.Metro{})
	if err != nil {
		return nil, err
	}

	if metrodb, ok := entity.(*models.Metro); ok {

		location := &infrav3.Location{
			Metadata: &commonv3.Metadata{
				Name:       metrodb.Name,
				ModifiedAt: timestamppb.New(metrodb.ModifiedAt),
			},
			Spec: &infrav3.Metro{
				Name:        metrodb.Name,
				Country:     metrodb.Country,
				City:        metrodb.City,
				State:       metrodb.State,
				Latitude:    metrodb.Latitude,
				Longitude:   metrodb.Longitude,
				StateCode:   metrodb.StateCode,
				CountryCode: metrodb.CountryCode,
			},
		}

		return location, nil

	}
	return &location, nil
}

func (s *metroService) Update(ctx context.Context, metro *infrav3.Location) (*infrav3.Location, error) {

	entity, err := dao.GetByName(ctx, s.db, metro.Metadata.Name, &models.Metro{})
	if err != nil {
		return metro, err
	}

	if metrodb, ok := entity.(*models.Metro); ok {
		//update metro details
		metrodb.City = metro.Spec.City
		metrodb.Country = metro.Spec.Country
		metrodb.State = metro.Spec.State
		metrodb.StateCode = metro.Spec.StateCode
		metrodb.CountryCode = metro.Spec.CountryCode
		metrodb.Latitude = metro.Spec.Latitude
		metrodb.Longitude = metro.Spec.Longitude
		metrodb.ModifiedAt = time.Now()

		_, err = dao.Update(ctx, s.db, metrodb.ID, metrodb)
		if err != nil {
			return metro, err
		}
	}

	return metro, nil
}

func (s *metroService) Delete(ctx context.Context, metro *infrav3.Location) (*infrav3.Location, error) {

	entity, err := dao.GetByName(ctx, s.db, metro.Metadata.Name, &models.Metro{})
	if err != nil {
		return metro, err
	}
	if metrodb, ok := entity.(*models.Metro); ok {
		err = dao.Delete(ctx, s.db, metrodb.ID, metrodb)
		if err != nil {
			return metro, err
		}
	}

	return metro, nil
}

func (s *metroService) List(ctx context.Context, partner string) (*infrav3.LocationList, error) {
	var locations infrav3.LocationList
	var metros []*infrav3.Metro
	var metrodbs []models.Metro

	var part models.Partner
	_, err := dao.GetByName(ctx, s.db, partner, &part)
	if err != nil {
		return nil, err
	}

	entities, err := dao.List(ctx, s.db, uuid.NullUUID{UUID: part.ID, Valid: true}, uuid.NullUUID{UUID: uuid.Nil, Valid: false}, &metrodbs)
	if err != nil {
		return nil, err
	}

	if metrodbs, ok := entities.(*[]models.Metro); ok {
		for _, metrodb := range *metrodbs {

			metro := &infrav3.Metro{
				Name:        metrodb.Name,
				City:        metrodb.City,
				State:       metrodb.State,
				Country:     metrodb.Country,
				Latitude:    metrodb.Latitude,
				Longitude:   metrodb.Longitude,
				StateCode:   metrodb.StateCode,
				CountryCode: metrodb.CountryCode,
			}
			metros = append(metros, metro)
		}

		locations = infrav3.LocationList{
			Metadata: &commonv3.ListMetadata{
				Count: int64(len(metros)),
			},
			Items: metros,
		}
	}

	return &locations, nil
}

func (s *metroService) GetIDByName(ctx context.Context, name string) (uuid.UUID, error) {
	entity, err := dao.GetByName(ctx, s.db, name, &models.Metro{})
	if err != nil {
		return uuid.Nil, err
	}

	if metrodb, ok := entity.(*models.Metro); ok {
		return metrodb.ID, nil
	}
	return uuid.Nil, nil
}
