package main

import (
	"context"
	"crypto/tls"
	"fmt"
	"log"
	"regexp"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/protobuf/types/known/structpb"
	"gorm.io/gorm"
	"src.whiteboxsystems.nl/DECOZO/okapi"
	"src.whiteboxsystems.nl/DECOZO/okapidemo/certgen"
	"src.whiteboxsystems.nl/DECOZO/okapidemo/his/model"
	"src.whiteboxsystems.nl/DECOZO/okapidemo/sharedmodel"
)

func toStruct(m map[string]interface{}) *structpb.Struct {
	s, err := structpb.NewStruct(m)

	if err != nil {
		panic(err)
	}

	return s
}

func getUnauthenticatedClient(addr string) (okapi.OkAPIClient, error) {
	opts := []grpc.DialOption{
		grpc.WithTransportCredentials(
			credentials.NewTLS(&tls.Config{InsecureSkipVerify: true}), // Don't do this in production
		),
	}

	conn, err := grpc.Dial(addr, opts...)

	if err != nil {
		return nil, err
	}

	// defer serviceProvider.Close()
	return okapi.NewOkAPIClient(conn), nil
}

func getAuthenticatedClient(serviceProvider *model.ServiceProvider, cert tls.Certificate) (okapi.OkAPIClient, error) {
	opts := []grpc.DialOption{
		grpc.WithTransportCredentials(
			credentials.NewTLS(&tls.Config{
				InsecureSkipVerify: true,
				Certificates:       []tls.Certificate{cert},
			}),
		),
	}
	conn, err := grpc.Dial(serviceProvider.Addr, opts...)

	if err != nil {
		return nil, err
	}

	return okapi.NewOkAPIClient(conn), nil
}

type PSKAuth struct {
	psk      string
	insecure bool
}

func (ma PSKAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
	return map[string]string{
		"authorization": ma.psk,
	}, nil
}

func (ma PSKAuth) RequireTransportSecurity() bool {
	return !ma.insecure
}

func makePSKAuth(psk string, insecure bool) *PSKAuth {
	return &PSKAuth{psk, insecure}
}

func (srv *HISServer) register(addr string) (*model.ServiceProvider, error) {
	client, err := getUnauthenticatedClient(addr)

	if err != nil {
		return nil, err
	}

	jwkBytes, err := certgen.PublicKeyToJWKJson(certgen.ExtractPublicKey(srv.clientCert.PrivateKey))

	if err != nil {
		return nil, err
	}

	auth := &okapi.XISAuthConfiguration{
		Method: okapi.XISAuthMethod_mTLS,
		Configuration: &okapi.XISAuthConfiguration_MtlsConfiguration{
			MtlsConfiguration: &okapi.MTLSConfigurationParams{
				PublicKey: string(jwkBytes),
			},
		},
	}

	resp, err := client.Register(context.Background(), &okapi.RegisterRequest{
		OrganisationIdentifier:     "00009999",
		OrganisationIdentifierType: "https://vektis.nl/agbz",
		OrganisationDisplayName:    "Praktijk de oude berg",
		Auth:                       auth,
	})

	if err != nil {
		log.Printf("Err in request: %v", err)
		return nil, err
	}

	connection := &model.ServiceProvider{
		Addr: addr,
		AuthConfig: &sharedmodel.XISAuthConfig{
			Method: int32(okapi.XISAuthMethod_mTLS),
			Raw:    string(jwkBytes),
		},
		State:     model.ConnectionStatePending,
		Reference: resp.Reference,
	}

	meta, _ := srv.listMeta(connection)

	connection.Supplier = meta.SupplierDisplayName
	connection.System = meta.ProductName

	if err := srv.data.Create(connection).Error; err != nil {
		return nil, err
	}

	return connection, nil
}

func (srv *HISServer) activate(serviceProvider *model.ServiceProvider, psk string) (*model.ServiceProvider, error) {
	client, err := getAuthenticatedClient(serviceProvider, srv.clientCert)
	if err != nil {
		panic(err)
	}

	_, err = client.CompleteRegistration(context.Background(), &okapi.CompleteRegistrationRequest{
		Reference:          serviceProvider.Reference,
		AuthorizationToken: psk,
	})

	if err != nil {
		log.Printf("Err in request: %v", err)
		return nil, err
	}

	meta, err := srv.listMeta(serviceProvider)

	if err != nil {
		return serviceProvider, fmt.Errorf("Failed to retreive metadata: %v ", err)
	}

	serviceProvider.State = model.ConnectionStateCompleted
	serviceProvider.Supplier = meta.SupplierDisplayName
	serviceProvider.System = meta.ProductName

	if err := srv.data.Save(serviceProvider).Error; err != nil {
		return nil, err
	}

	return serviceProvider, err

}

func (srv *HISServer) listMeta(serviceProvider *model.ServiceProvider) (*okapi.GetMetadataResponse, error) {
	client, err := getUnauthenticatedClient(serviceProvider.Addr)
	if err != nil {
		return nil, err
	}

	resp, err := client.GetMetadata(context.Background(), &okapi.GetMetadataRequest{})

	if err != nil {
		log.Printf("Err in request: %v", err)
		return nil, err
	}

	return resp, nil
}

func (srv *HISServer) listServices(serviceProvider *model.ServiceProvider) (*okapi.ListServicesResponse, error) {
	client, err := getAuthenticatedClient(serviceProvider, srv.clientCert)
	if err != nil {
		return nil, err
	}

	resp, err := client.ListServices(context.Background(), &okapi.ListServicesRequest{})

	if err != nil {
		log.Printf("Err in request: %v", err)
		return nil, err
	}

	return resp, nil
}

func (srv *HISServer) enableService(serviceProvider *model.ServiceProvider, serviceId string) error {
	client, err := getAuthenticatedClient(serviceProvider, srv.clientCert)
	if err != nil {
		return err
	}

	moddedService := &model.Service{}
	moddedServiceErr := srv.data.Where("service_provider_id = ? and service_id = ?", serviceProvider.ID, serviceId).First(moddedService).Error

	if moddedServiceErr != nil && moddedServiceErr != gorm.ErrRecordNotFound {
		return moddedServiceErr
	}

	if moddedServiceErr == nil {
		return fmt.Errorf("Service already activivated")
	}

	meta, _ := srv.listServices(serviceProvider)
	var serviceDefinition *okapi.Service

	for _, sd := range meta.Services {
		if sd.Id == serviceId {
			serviceDefinition = sd
		}
	}

	if serviceDefinition == nil {
		return fmt.Errorf("Invalid service: %v", serviceId)
	}

	var resp *okapi.EnableServiceResponse

	if m, _ := regexp.MatchString("wbx:*", serviceId); m { // Whitebox
		resp, err = client.EnableService(context.Background(), &okapi.EnableServiceRequest{
			ServiceId: serviceId,
			Fetch: &okapi.CallbackConfiguration{
				Protocol: "https://whiteboxsystems.nl/protospecs/whitebox-fetch/http",
				Configuration: toStruct(map[string]interface{}{
					"url": externalAddr + "/external/api",
				}),
				Auth: &okapi.ProtocolAuthConfiguration{
					Method: sharedmodel.AuthMethodDecozoMTLS,
				},
			},
			Push: &okapi.CallbackConfiguration{
				Protocol: "https://whiteboxsystems.nl/protospecs/whitebox-push/http",
				Configuration: toStruct(map[string]interface{}{
					"url": externalAddr + "/external/api",
				}),
				Auth: &okapi.ProtocolAuthConfiguration{
					Method: sharedmodel.AuthMethodDecozoMTLS,
				},
			},
		})
	} else { // DVZA / FHIR
		resp, err = client.EnableService(context.Background(), &okapi.EnableServiceRequest{
			ServiceId: serviceId,
			Fetch: &okapi.CallbackConfiguration{
				Protocol: "https://hl7.org/fhir",
				Configuration: toStruct(map[string]interface{}{
					"url": externalAddr + "/external/fhir/Patient",
				}),
				Auth: &okapi.ProtocolAuthConfiguration{
					Method: sharedmodel.AuthMethodDecozoBearerToken,
				},
			},
			Push: &okapi.CallbackConfiguration{
				Protocol: "https://hl7.org/fhir",
				Configuration: toStruct(map[string]interface{}{
					"url": externalAddr + "/external/fhir/Patient",
				}),
				Auth: &okapi.ProtocolAuthConfiguration{
					Method: sharedmodel.AuthMethodDecozoBearerToken,
				},
			},
		})
	}

	if err != nil {
		log.Printf("Err in request: %v", err)
		return err
	}

	authConfig := sharedmodel.NewAuthConfig(resp.Fetch.Auth)

	return srv.data.Create(&model.Service{
		ServiceProviderID:  serviceProvider.ID,
		ServiceID:          serviceDefinition.Id,
		Name:               serviceDefinition.Name,
		Description:        serviceDefinition.Name,
		SubscriptionPolicy: serviceDefinition.SubscriptionPolicy,
		ConsentPolicy:      serviceDefinition.ConsentPolicy,
		AuthConfig:         authConfig,
	}).Error
}

func (srv *HISServer) disableService(serviceProvider *model.ServiceProvider, serviceId string) error {
	client, err := getAuthenticatedClient(serviceProvider, srv.clientCert)
	if err != nil {
		return err
	}

	moddedService := &model.Service{}
	moddedServiceErr := srv.data.Where("service_provider_id = ? and service_id = ?", serviceProvider.ID, serviceId).First(moddedService).Error

	if moddedServiceErr != nil && moddedServiceErr != gorm.ErrRecordNotFound {
		return moddedServiceErr
	}

	if moddedServiceErr != nil {
		return fmt.Errorf("Service not active")
	}

	meta, _ := srv.listServices(serviceProvider)
	var serviceDefinition *okapi.Service

	for _, sd := range meta.Services {
		if sd.Id == serviceId {
			serviceDefinition = sd
		}
	}

	if serviceDefinition == nil {
		return fmt.Errorf("Invalid service: %v", serviceId)
	}

	_, err = client.DisableService(context.Background(), &okapi.DisableServiceRequest{
		ServiceId: serviceId,
	})

	if err != nil {
		log.Printf("Err in request: %v", err)
		return err
	}

	subs := []model.Patient{}

	srv.data.Model(moddedService).Association("Subscriptions").Find(&subs)
	srv.data.Model(moddedService).Association("Subscriptions").Delete(subs)
	srv.unsubscribePatients(serviceProvider, moddedService, subs)
	return srv.data.Unscoped().Delete(moddedService).Error
}

func (srv *HISServer) subscribePatients(serviceProvider *model.ServiceProvider, service *model.Service, patients []model.Patient) (*okapi.CreateOrUpdatePatientRegistrationsResponse, error) {
	client, err := getAuthenticatedClient(serviceProvider, srv.clientCert)
	if err != nil {
		return nil, err
	}

	subs := []*okapi.PatientRegistrationCreateOrUpdateData{}

	for _, pat := range patients {
		subs = append(subs, &okapi.PatientRegistrationCreateOrUpdateData{
			Id: pat.ExternalId,
			Subject: &okapi.PatientMeta{
				Identifier: &okapi.Identifier{
					Type:  "http://fhir.nl/fhir/NamingSystem/bsn",
					Value: pat.ExternalId,
				},
				Name: &okapi.Name{
					Display: pat.Name,
				},
				Address:   &okapi.Address{},
				Birthdate: pat.Birthdate,
			},
			CallbackProtocolData: toStruct(map[string]interface{}{
				"patientID": pat.PatientID,
			}),
		})
	}

	req := &okapi.CreateOrUpdatePatientRegistrationsRequest{
		ServiceId:     service.ServiceID,
		Registrations: subs,
	}

	resp, err := client.CreateOrUpdatePatientRegistrations(context.Background(), req)

	if err != nil {
		log.Printf("Err in request: %v", err)
		return nil, err
	}

	return resp, nil
}

func (srv *HISServer) unsubscribePatients(serviceProvider *model.ServiceProvider, service *model.Service, patients []model.Patient) (*okapi.RemovePatientRegistrationsResponse, error) {
	client, err := getAuthenticatedClient(serviceProvider, srv.clientCert)
	if err != nil {
		return nil, err
	}

	subs := []string{}

	for _, pat := range patients {
		subs = append(subs, pat.ExternalId)
	}

	req := &okapi.RemovePatientRegistrationsRequest{
		ServiceId:     service.ServiceID,
		Registrations: subs,
	}

	resp, err := client.RemovePatientRegistrations(context.Background(), req)

	if err != nil {
		log.Printf("Err in request: %v", err)
		return nil, err
	}

	return resp, nil
}

func (srv *HISServer) listPatientRegistrations(serviceProvider *model.ServiceProvider, service *model.Service) ([]*okapi.PatientRegistrationData, error) {
	client, err := getAuthenticatedClient(serviceProvider, srv.clientCert)
	if err != nil {
		return nil, err
	}

	req := &okapi.ListPatientRegistrationsRequest{
		ServiceId: service.ServiceID,
	}

	resp, err := client.ListPatientRegistrations(context.Background(), req)

	if err != nil {
		log.Printf("Err in request: %v", err)
		return nil, err
	}

	return resp.PatientRegistrationData, nil
}
