package sql

import (
	"encoding/xml"
	"fmt"
	"time"

	"github.com/Azure/azure-sdk-for-go/management"
)

// Definitions of numerous constants representing API endpoints.
const (
	azureCreateDatabaseServerURL = "services/sqlservers/servers"
	azureListDatabaseServersURL  = "services/sqlservers/servers"
	azureDeleteDatabaseServerURL = "services/sqlservers/servers/%s"

	azureCreateFirewallRuleURL = "services/sqlservers/servers/%s/firewallrules"
	azureGetFirewallRuleURL    = "services/sqlservers/servers/%s/firewallrules/%s"
	azureListFirewallRulesURL  = "services/sqlservers/servers/%s/firewallrules"
	azureUpdateFirewallRuleURL = "services/sqlservers/servers/%s/firewallrules/%s"
	azureDeleteFirewallRuleURL = "services/sqlservers/servers/%s/firewallrules/%s"

	azureCreateDatabaseURL = "services/sqlservers/servers/%s/databases"
	azureGetDatabaseURL    = "services/sqlservers/servers/%s/databases/%s"
	azureListDatabasesURL  = "services/sqlservers/servers/%s/databases?contentview=generic"
	azureUpdateDatabaseURL = "services/sqlservers/servers/%s/databases/%s"
	azureDeleteDatabaseURL = "services/sqlservers/servers/%s/databases/%s"

	errParamNotSpecified = "Parameter %s was not specified."

	DatabaseStateCreating = "Creating"
)

// SQLDatabaseClient defines various database CRUD operations.
// It contains a management.Client for making the actual http calls.
type SQLDatabaseClient struct {
	mgmtClient management.Client
}

// NewClient returns a new SQLDatabaseClient struct with the provided
// management.Client as the underlying client.
func NewClient(mgmtClient management.Client) SQLDatabaseClient {
	return SQLDatabaseClient{mgmtClient}
}

// CreateServer creates a new Azure SQL Database server and return its name.
//
// https://msdn.microsoft.com/en-us/library/azure/dn505699.aspx
func (c SQLDatabaseClient) CreateServer(params DatabaseServerCreateParams) (string, error) {
	req, err := xml.Marshal(params)
	if err != nil {
		return "", err
	}

	resp, err := c.mgmtClient.SendAzurePostRequestWithReturnedResponse(azureCreateDatabaseServerURL, req)
	if err != nil {
		return "", err
	}

	var name string
	err = xml.Unmarshal(resp, &name)

	return name, err
}

// ListServers retrieves the Azure SQL Database servers for this subscription.
//
// https://msdn.microsoft.com/en-us/library/azure/dn505702.aspx
func (c SQLDatabaseClient) ListServers() (ListServersResponse, error) {
	var resp ListServersResponse

	data, err := c.mgmtClient.SendAzureGetRequest(azureListDatabaseServersURL)
	if err != nil {
		return resp, err
	}

	err = xml.Unmarshal(data, &resp)
	return resp, err
}

// DeleteServer deletes an Azure SQL Database server (including all its databases).
//
// https://msdn.microsoft.com/en-us/library/azure/dn505695.aspx
func (c SQLDatabaseClient) DeleteServer(name string) error {
	if name == "" {
		return fmt.Errorf(errParamNotSpecified, "name")
	}

	url := fmt.Sprintf(azureDeleteDatabaseServerURL, name)
	_, err := c.mgmtClient.SendAzureDeleteRequest(url)
	return err
}

// CreateFirewallRule creates an Azure SQL Database server
// firewall rule.
//
// https://msdn.microsoft.com/en-us/library/azure/dn505712.aspx
func (c SQLDatabaseClient) CreateFirewallRule(server string, params FirewallRuleCreateParams) error {
	if server == "" {
		return fmt.Errorf(errParamNotSpecified, "server")
	}

	req, err := xml.Marshal(params)
	if err != nil {
		return err
	}

	url := fmt.Sprintf(azureCreateFirewallRuleURL, server)

	_, err = c.mgmtClient.SendAzurePostRequest(url, req)
	return err
}

// GetFirewallRule gets the details of an Azure SQL Database Server firewall rule.
//
// https://msdn.microsoft.com/en-us/library/azure/dn505698.aspx
func (c SQLDatabaseClient) GetFirewallRule(server, ruleName string) (FirewallRuleResponse, error) {
	var rule FirewallRuleResponse

	if server == "" {
		return rule, fmt.Errorf(errParamNotSpecified, "server")
	}
	if ruleName == "" {
		return rule, fmt.Errorf(errParamNotSpecified, "ruleName")
	}

	url := fmt.Sprintf(azureGetFirewallRuleURL, server, ruleName)
	resp, err := c.mgmtClient.SendAzureGetRequest(url)
	if err != nil {
		return rule, err
	}

	err = xml.Unmarshal(resp, &rule)
	return rule, err
}

// ListFirewallRules retrieves the set of firewall rules for an Azure SQL
// Database Server.
//
// https://msdn.microsoft.com/en-us/library/azure/dn505715.aspx
func (c SQLDatabaseClient) ListFirewallRules(server string) (ListFirewallRulesResponse, error) {
	var rules ListFirewallRulesResponse

	if server == "" {
		return rules, fmt.Errorf(errParamNotSpecified, "server")
	}

	url := fmt.Sprintf(azureListFirewallRulesURL, server)
	resp, err := c.mgmtClient.SendAzureGetRequest(url)
	if err != nil {
		return rules, err
	}

	err = xml.Unmarshal(resp, &rules)
	return rules, err
}

// UpdateFirewallRule update a firewall rule for an Azure SQL Database server.
//
// https://msdn.microsoft.com/en-us/library/azure/dn505707.aspx
func (c SQLDatabaseClient) UpdateFirewallRule(server, ruleName string, params FirewallRuleUpdateParams) error {
	if server == "" {
		return fmt.Errorf(errParamNotSpecified, "server")
	}
	if ruleName == "" {
		return fmt.Errorf(errParamNotSpecified, "ruleName")
	}

	req, err := xml.Marshal(params)
	if err != nil {
		return err
	}

	url := fmt.Sprintf(azureUpdateFirewallRuleURL, server, ruleName)
	_, err = c.mgmtClient.SendAzurePutRequest(url, "text/xml", req)
	return err
}

// DeleteFirewallRule deletes an Azure SQL Database server firewall rule.
//
// https://msdn.microsoft.com/en-us/library/azure/dn505706.aspx
func (c SQLDatabaseClient) DeleteFirewallRule(server, ruleName string) error {
	if server == "" {
		return fmt.Errorf(errParamNotSpecified, "server")
	}
	if ruleName == "" {
		return fmt.Errorf(errParamNotSpecified, "ruleName")
	}

	url := fmt.Sprintf(azureDeleteFirewallRuleURL, server, ruleName)

	_, err := c.mgmtClient.SendAzureDeleteRequest(url)
	return err
}

// CreateDatabase creates a new Microsoft Azure SQL Database on the given database server.
//
// https://msdn.microsoft.com/en-us/library/azure/dn505701.aspx
func (c SQLDatabaseClient) CreateDatabase(server string, params DatabaseCreateParams) error {
	if server == "" {
		return fmt.Errorf(errParamNotSpecified, "server")
	}

	req, err := xml.Marshal(params)
	if err != nil {
		return err
	}

	target := fmt.Sprintf(azureCreateDatabaseURL, server)
	_, err = c.mgmtClient.SendAzurePostRequest(target, req)
	return err
}

// WaitForDatabaseCreation is a helper method which waits
// for the creation of the database on the given server.
func (c SQLDatabaseClient) WaitForDatabaseCreation(
	server, database string,
	cancel chan struct{}) error {
	for {
		stat, err := c.GetDatabase(server, database)
		if err != nil {
			return err
		}
		if stat.State != DatabaseStateCreating {
			return nil
		}

		select {
		case <-time.After(management.DefaultOperationPollInterval):
		case <-cancel:
			return management.ErrOperationCancelled
		}
	}
}

// GetDatabase gets the details for an Azure SQL Database.
//
// https://msdn.microsoft.com/en-us/library/azure/dn505708.aspx
func (c SQLDatabaseClient) GetDatabase(server, database string) (ServiceResource, error) {
	var db ServiceResource

	if database == "" {
		return db, fmt.Errorf(errParamNotSpecified, "database")
	}
	if server == "" {
		return db, fmt.Errorf(errParamNotSpecified, "server")
	}

	url := fmt.Sprintf(azureGetDatabaseURL, server, database)
	resp, err := c.mgmtClient.SendAzureGetRequest(url)
	if err != nil {
		return db, err
	}

	err = xml.Unmarshal(resp, &db)
	return db, err
}

// ListDatabases returns a list of Azure SQL Databases on the given server.
//
// https://msdn.microsoft.com/en-us/library/azure/dn505711.aspx
func (c SQLDatabaseClient) ListDatabases(server string) (ListDatabasesResponse, error) {
	var databases ListDatabasesResponse
	if server == "" {
		return databases, fmt.Errorf(errParamNotSpecified, "server name")
	}

	url := fmt.Sprintf(azureListDatabasesURL, server)
	resp, err := c.mgmtClient.SendAzureGetRequest(url)
	if err != nil {
		return databases, err
	}

	err = xml.Unmarshal(resp, &databases)
	return databases, err
}

// UpdateDatabase updates the details of the given Database off the given server.
//
// https://msdn.microsoft.com/en-us/library/azure/dn505718.aspx
func (c SQLDatabaseClient) UpdateDatabase(
	server, database string,
	params ServiceResourceUpdateParams) (management.OperationID, error) {
	if database == "" {
		return "", fmt.Errorf(errParamNotSpecified, "database")
	}
	if server == "" {
		return "", fmt.Errorf(errParamNotSpecified, "server")
	}

	url := fmt.Sprintf(azureUpdateDatabaseURL, server, database)
	req, err := xml.Marshal(params)
	if err != nil {
		return "", err
	}

	return c.mgmtClient.SendAzurePutRequest(url, "text/xml", req)
}

// DeleteDatabase deletes the Azure SQL Database off the given server.
//
// https://msdn.microsoft.com/en-us/library/azure/dn505705.aspx
func (c SQLDatabaseClient) DeleteDatabase(server, database string) error {
	if database == "" {
		return fmt.Errorf(errParamNotSpecified, "database")
	}
	if server == "" {
		return fmt.Errorf(errParamNotSpecified, "server")
	}

	url := fmt.Sprintf(azureDeleteDatabaseURL, server, database)

	_, err := c.mgmtClient.SendAzureDeleteRequest(url)

	return err
}
