package godata

import (
	"net/http"
	"net/url"
	"strings"
)

const (
	ODataFieldContext string = "@odata.context"
	ODataFieldCount   string = "@odata.count"
	ODataFieldValue   string = "value"
)

// The basic interface for a GoData provider. All providers must implement
// these functions.
type GoDataProvider interface {
	// Request a single entity from the provider. Should return a response field
	// that contains the value mapping properties to values for the entity.
	GetEntity(*GoDataRequest) (*GoDataResponseField, error)
	// Request a collection of entities from the provider. Should return a
	// response field that contains the value of a slice of every entity in the
	// collection filtered by the request query parameters.
	GetEntityCollection(*GoDataRequest) (*GoDataResponseField, error)
	// Request the number of entities in a collection, disregarding any filter
	// query parameters.
	GetCount(*GoDataRequest) (int, error)
	// Get the object model representation from the provider.
	GetMetadata() *GoDataMetadata
}

// A GoDataService will spawn an HTTP listener, which will connect GoData
// requests with a backend provider given to it.
type GoDataService struct {
	// The base url for the service. Navigating to this URL will display the
	// service document.
	BaseUrl *url.URL
	// The provider for this service that is serving the data to the OData API.
	Provider GoDataProvider
	// Metadata cache taken from the provider.
	Metadata *GoDataMetadata
	// A mapping from schema names to schema references
	SchemaLookup map[string]*GoDataSchema
	// A bottom-up mapping from entity type names to schema namespaces to
	// the entity type reference
	EntityTypeLookup map[string]map[string]*GoDataEntityType
	// A bottom-up mapping from entity container names to schema namespaces to
	// the entity container reference
	EntityContainerLookup map[string]map[string]*GoDataEntityContainer
	// A bottom-up mapping from entity set names to entity collection names to
	// schema namespaces to the entity set reference
	EntitySetLookup map[string]map[string]map[string]*GoDataEntitySet
	// A lookup for entity properties if an entity type is given, lookup
	// properties by name
	PropertyLookup map[*GoDataEntityType]map[string]*GoDataProperty
	// A lookup for navigational properties if an entity type is given,
	// lookup navigational properties by name
	NavigationPropertyLookup map[*GoDataEntityType]map[string]*GoDataNavigationProperty
}

type providerChannelResponse struct {
	Field *GoDataResponseField
	Error error
}

// Create a new service from a given provider. This step builds lookups for
// all parts of the data model, so constant time lookups can be performed. This
// step only happens once when the server starts up, so the overall cost is
// minimal. The given url will be treated as the base URL for all service
// requests, and used for building context URLs, etc.
func BuildService(provider GoDataProvider, serviceUrl string) (*GoDataService, error) {
	metadata := provider.GetMetadata()

	// build the lookups from the metadata
	schemaLookup := map[string]*GoDataSchema{}
	entityLookup := map[string]map[string]*GoDataEntityType{}
	containerLookup := map[string]map[string]*GoDataEntityContainer{}
	entitySetLookup := map[string]map[string]map[string]*GoDataEntitySet{}
	propertyLookup := map[*GoDataEntityType]map[string]*GoDataProperty{}
	navPropLookup := map[*GoDataEntityType]map[string]*GoDataNavigationProperty{}

	for _, schema := range metadata.DataServices.Schemas {
		schemaLookup[schema.Namespace] = schema

		for _, entity := range schema.EntityTypes {
			if _, ok := entityLookup[entity.Name]; !ok {
				entityLookup[entity.Name] = map[string]*GoDataEntityType{}
			}
			if _, ok := propertyLookup[entity]; !ok {
				propertyLookup[entity] = map[string]*GoDataProperty{}
			}
			if _, ok := navPropLookup[entity]; !ok {
				navPropLookup[entity] = map[string]*GoDataNavigationProperty{}
			}
			entityLookup[entity.Name][schema.Namespace] = entity

			for _, prop := range entity.Properties {
				propertyLookup[entity][prop.Name] = prop
			}
			for _, prop := range entity.NavigationProperties {
				navPropLookup[entity][prop.Name] = prop
			}
		}

		for _, container := range schema.EntityContainers {
			if _, ok := containerLookup[container.Name]; !ok {
				containerLookup[container.Name] = map[string]*GoDataEntityContainer{}
			}
			containerLookup[container.Name][schema.Namespace] = container

			for _, set := range container.EntitySets {
				if _, ok := entitySetLookup[set.Name]; !ok {
					entitySetLookup[set.Name] = map[string]map[string]*GoDataEntitySet{}
				}
				if _, ok := entitySetLookup[set.Name][container.Name]; !ok {
					entitySetLookup[set.Name][container.Name] = map[string]*GoDataEntitySet{}
				}
				entitySetLookup[set.Name][container.Name][schema.Namespace] = set
			}
		}
	}

	parsedUrl, err := url.Parse(serviceUrl)

	if err != nil {
		return nil, err
	}

	return &GoDataService{
		parsedUrl,
		provider,
		provider.GetMetadata(),
		schemaLookup,
		entityLookup,
		containerLookup,
		entitySetLookup,
		propertyLookup,
		navPropLookup,
	}, nil
}

// The default handler for parsing requests as GoDataRequests, passing them
// to a GoData provider, and then building a response.
func (service *GoDataService) GoDataHTTPHandler(w http.ResponseWriter, r *http.Request) {

	request, err := ParseRequest(r.URL.Path, r.URL.Query())

	if err != nil {
		panic(err) // TODO: return proper error
	}

	// Semanticize all tokens in the request, connecting them with their
	// corresponding types in the service
	err = SemanticizeRequest(request, service)

	if err != nil {
		panic(err) // TODO: return proper error
	}

	// TODO: differentiate GET and POST requests
	var response []byte = []byte{}
	if request.RequestKind == RequestKindMetadata {
		response, err = service.buildMetadataResponse(request)
	} else if request.RequestKind == RequestKindService {
		response, err = service.buildServiceResponse(request)
	} else if request.RequestKind == RequestKindCollection {
		response, err = service.buildCollectionResponse(request)
	} else if request.RequestKind == RequestKindEntity {
		response, err = service.buildEntityResponse(request)
	} else if request.RequestKind == RequestKindProperty {
		response, err = service.buildPropertyResponse(request)
	} else if request.RequestKind == RequestKindPropertyValue {
		response, err = service.buildPropertyValueResponse(request)
	} else if request.RequestKind == RequestKindCount {
		response, err = service.buildCountResponse(request)
	} else if request.RequestKind == RequestKindRef {
		response, err = service.buildRefResponse(request)
	} else {
		err = NotImplementedError("Request type not understood.")
	}

	if err != nil {
		panic(err) // TODO: return proper error
	}

	w.Write(response)
}

func (service *GoDataService) buildMetadataResponse(request *GoDataRequest) ([]byte, error) {
	return service.Metadata.Bytes()
}

func (service *GoDataService) buildServiceResponse(request *GoDataRequest) ([]byte, error) {
	// TODO
	return nil, NotImplementedError("Service responses are not implemented yet.")
}

func (service *GoDataService) buildCollectionResponse(request *GoDataRequest) ([]byte, error) {
	response := &GoDataResponse{Fields: map[string]*GoDataResponseField{}}
	// get request from provider
	responses := make(chan *providerChannelResponse)
	go func() {
		result, err := service.Provider.GetEntityCollection(request)
		responses <- &providerChannelResponse{result, err}
		close(responses)
	}()

	if bool(*request.Query.Count) {
		// if count is true, also include the count result
		counts := make(chan *providerChannelResponse)

		go func() {
			result, err := service.Provider.GetCount(request)
			counts <- &providerChannelResponse{&GoDataResponseField{result}, err}
			close(counts)
		}()

		r := <-counts
		if r.Error != nil {
			return nil, r.Error
		}

		response.Fields[ODataFieldCount] = r.Field
	}
	// build context URL
	context := request.LastSegment.SemanticReference.(*GoDataEntitySet).Name
	path, err := url.Parse("./$metadata#" + context)
	if err != nil {
		return nil, err
	}
	contextUrl := service.BaseUrl.ResolveReference(path).String()
	response.Fields[ODataFieldContext] = &GoDataResponseField{Value: contextUrl}

	// wait for a response from the provider
	r := <-responses

	if r.Error != nil {
		return nil, r.Error
	}

	response.Fields[ODataFieldValue] = r.Field

	return response.Json()
}

func (service *GoDataService) buildEntityResponse(request *GoDataRequest) ([]byte, error) {
	// get request from provider
	responses := make(chan *providerChannelResponse)
	go func() {
		result, err := service.Provider.GetEntity(request)
		responses <- &providerChannelResponse{result, err}
		close(responses)
	}()

	// build context URL
	context := request.LastSegment.SemanticReference.(*GoDataEntitySet).Name
	path, err := url.Parse("./$metadata#" + context + "/$entity")
	if err != nil {
		return nil, err
	}
	contextUrl := service.BaseUrl.ResolveReference(path).String()

	// wait for a response from the provider
	r := <-responses

	if r.Error != nil {
		return nil, r.Error
	}

	// Add context field to result and create the response
	switch r.Field.Value.(type) {
	case map[string]*GoDataResponseField:
		fields := r.Field.Value.(map[string]*GoDataResponseField)
		fields[ODataFieldContext] = &GoDataResponseField{Value: contextUrl}
		response := &GoDataResponse{Fields: fields}

		return response.Json()
	default:
		return nil, InternalServerError("Provider did not return a valid response" +
			" from GetEntity()")
	}
}

func (service *GoDataService) buildPropertyResponse(request *GoDataRequest) ([]byte, error) {
	// TODO
	return nil, NotImplementedError("Property responses are not implemented yet.")
}

func (service *GoDataService) buildPropertyValueResponse(request *GoDataRequest) ([]byte, error) {
	// TODO
	return nil, NotImplementedError("Property value responses are not implemented yet.")
}

func (service *GoDataService) buildCountResponse(request *GoDataRequest) ([]byte, error) {
	// get request from provider
	responses := make(chan *providerChannelResponse)
	go func() {
		result, err := service.Provider.GetCount(request)
		responses <- &providerChannelResponse{&GoDataResponseField{result}, err}
		close(responses)
	}()

	// wait for a response from the provider
	r := <-responses

	if r.Error != nil {
		return nil, r.Error
	}

	return r.Field.Json()
}

func (service *GoDataService) buildRefResponse(request *GoDataRequest) ([]byte, error) {
	// TODO
	return nil, NotImplementedError("Ref responses are not implemented yet.")
}

// Start the service listening on the given address.
func (service *GoDataService) ListenAndServe(addr string) {
	http.HandleFunc("/", service.GoDataHTTPHandler)
	http.ListenAndServe(addr, nil)
}

// Lookup an entity type from the service metadata. Accepts a fully qualified
// name, e.g., ODataService.EntityTypeName or, if unambiguous, accepts a
// simple identifier, e.g., EntityTypeName.
func (service *GoDataService) LookupEntityType(name string) (*GoDataEntityType, error) {
	// strip "Collection()" and just return the raw entity type
	// The provider should keep track of what are collections and what aren't
	if strings.Contains(name, "(") && strings.Contains(name, ")") {
		name = name[strings.Index(name, "(")+1 : strings.LastIndex(name, ")")]
	}

	parts := strings.Split(name, ".")
	entityName := parts[len(parts)-1]
	// remove entity from the list of parts
	parts = parts[:len(parts)-1]

	schemas, ok := service.EntityTypeLookup[entityName]
	if !ok {
		return nil, BadRequestError("Entity " + name + " does not exist.")
	}

	if len(parts) > 0 {
		// namespace is provided
		entity, ok := schemas[parts[len(parts)-1]]
		if !ok {
			return nil, BadRequestError("Entity " + name + " not found in given namespace.")
		}
		return entity, nil
	} else {
		// no namespace, just return the first one

		// throw error if ambiguous
		if len(schemas) > 1 {
			return nil, BadRequestError("Entity " + name + " is ambiguous. Please provide a namespace.")
		}

		for _, v := range schemas {
			return v, nil
		}
	}

	// If this happens, that's very bad
	return nil, BadRequestError("No schema lookup found for entity " + name)
}

// Lookup an entity set from the service metadata. Accepts a fully qualified
// name, e.g., ODataService.ContainerName.EntitySetName,
// ContainerName.EntitySetName or, if unambiguous, accepts a  simple identifier,
// e.g., EntitySetName.
func (service *GoDataService) LookupEntitySet(name string) (*GoDataEntitySet, error) {
	parts := strings.Split(name, ".")
	setName := parts[len(parts)-1]
	// remove entity set from the list of parts
	parts = parts[:len(parts)-1]

	containers, ok := service.EntitySetLookup[setName]
	if !ok {
		return nil, BadRequestError("Entity set " + name + " does not exist.")
	}

	if len(parts) > 0 {
		// container is provided
		schemas, ok := containers[parts[len(parts)-1]]
		if !ok {
			return nil, BadRequestError("Container " + name + " not found.")
		}

		// remove container name from the list of parts
		parts = parts[:len(parts)-1]

		if len(parts) > 0 {
			// schema is provided
			set, ok := schemas[parts[len(parts)-1]]
			if !ok {
				return nil, BadRequestError("Entity set " + name + " not found.")
			}
			return set, nil
		} else {
			// no schema is provided

			if len(schemas) > 1 {
				// container is ambiguous
				return nil, BadRequestError("Entity set " + name + " is ambiguous. Please provide fully qualified name.")
			}

			// there should be one schema, if not then something went horribly wrong
			for _, set := range schemas {
				return set, nil
			}
		}

	} else {
		// no container is provided

		// return error if entity set is ambiguous
		if len(containers) > 1 {
			return nil, BadRequestError("Entity set " + name + " is ambiguous. Please provide fully qualified name.")
		}

		// find the first schema, it will be the only one
		for _, schemas := range containers {
			if len(schemas) > 1 {
				// container is ambiguous
				return nil, BadRequestError("Entity set " + name + " is ambiguous. Please provide fully qualified name.")
			}

			// there should be one schema, if not then something went horribly wrong
			for _, set := range schemas {
				return set, nil
			}
		}
	}
	return nil, BadRequestError("Entity set " + name + " not found.")
}