package vault

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/porter-dev/porter/internal/models/integrations"
	"github.com/porter-dev/porter/internal/repository/credentials"
)

// Client contains an API client for IronPlans
type Client struct {
	apiKey       string
	serverURL    string
	secretPrefix string
	httpClient   *http.Client
}

// NewClient creates a new billing API client
func NewClient(serverURL, apiKey, secretPrefix string) *Client {
	httpClient := &http.Client{
		Timeout: time.Minute,
	}

	return &Client{apiKey, serverURL, secretPrefix, httpClient}
}

func (c *Client) WriteOAuthCredential(
	oauthIntegration *integrations.OAuthIntegration,
	data *credentials.OAuthCredential,
) error {
	reqData := &CreateVaultSecretRequest{
		Data: data,
	}

	return c.postRequest(fmt.Sprintf("/v1/%s", c.getOAuthCredentialPath(oauthIntegration)), reqData, nil)
}

func (c *Client) GetOAuthCredential(oauthIntegration *integrations.OAuthIntegration) (*credentials.OAuthCredential, error) {
	resp := &GetOAuthCredentialResponse{}

	err := c.getRequest(fmt.Sprintf("/v1/%s", c.getOAuthCredentialPath(oauthIntegration)), resp)
	if err != nil {
		return nil, err
	}

	return resp.Data.Data, nil
}

func (c *Client) CreateOAuthToken(oauthIntegration *integrations.OAuthIntegration) (string, error) {
	credPath := c.getOAuthCredentialPath(oauthIntegration)
	policyName := fmt.Sprintf("access-%d-oauth-%d", oauthIntegration.ProjectID, oauthIntegration.ID)

	return c.getToken(credPath, policyName)
}

func (c *Client) getOAuthCredentialPath(oauthIntegration *integrations.OAuthIntegration) string {
	return fmt.Sprintf(
		"kv/data/secret/%s/%d/oauth/%d",
		c.secretPrefix,
		oauthIntegration.ProjectID,
		oauthIntegration.ID,
	)
}

func (c *Client) WriteGCPCredential(
	gcpIntegration *integrations.GCPIntegration,
	data *credentials.GCPCredential,
) error {
	reqData := &CreateVaultSecretRequest{
		Data: data,
	}

	return c.postRequest(fmt.Sprintf("/v1/%s", c.getGCPCredentialPath(gcpIntegration)), reqData, nil)
}

func (c *Client) GetGCPCredential(gcpIntegration *integrations.GCPIntegration) (*credentials.GCPCredential, error) {
	resp := &GetGCPCredentialResponse{}

	err := c.getRequest(fmt.Sprintf("/v1/%s", c.getGCPCredentialPath(gcpIntegration)), resp)
	if err != nil {
		return nil, err
	}

	return resp.Data.Data, nil
}

func (c *Client) CreateGCPToken(gcpIntegration *integrations.GCPIntegration) (string, error) {
	credPath := c.getGCPCredentialPath(gcpIntegration)
	policyName := fmt.Sprintf("access-%d-gcp-%d", gcpIntegration.ProjectID, gcpIntegration.ID)

	return c.getToken(credPath, policyName)
}

func (c *Client) getGCPCredentialPath(gcpIntegration *integrations.GCPIntegration) string {
	return fmt.Sprintf(
		"kv/data/secret/%s/%d/gcp/%d",
		c.secretPrefix,
		gcpIntegration.ProjectID,
		gcpIntegration.ID,
	)
}

func (c *Client) WriteAWSCredential(
	awsIntegration *integrations.AWSIntegration,
	data *credentials.AWSCredential,
) error {
	reqData := &CreateVaultSecretRequest{
		Data: data,
	}

	return c.postRequest(fmt.Sprintf("/v1/%s", c.getAWSCredentialPath(awsIntegration)), reqData, nil)
}

func (c *Client) GetAWSCredential(awsIntegration *integrations.AWSIntegration) (*credentials.AWSCredential, error) {
	resp := &GetAWSCredentialResponse{}

	err := c.getRequest(fmt.Sprintf("/v1/%s", c.getAWSCredentialPath(awsIntegration)), resp)
	if err != nil {
		return nil, err
	}

	return resp.Data.Data, nil
}

func (c *Client) CreateAWSToken(awsIntegration *integrations.AWSIntegration) (string, error) {
	credPath := c.getAWSCredentialPath(awsIntegration)
	policyName := fmt.Sprintf("access-%d-aws-%d", awsIntegration.ProjectID, awsIntegration.ID)

	return c.getToken(credPath, policyName)
}

func (c *Client) getAWSCredentialPath(awsIntegration *integrations.AWSIntegration) string {
	return fmt.Sprintf(
		"kv/data/secret/%s/%d/aws/%d",
		c.secretPrefix,
		awsIntegration.ProjectID,
		awsIntegration.ID,
	)
}

func (c *Client) WriteAzureCredential(
	azIntegration *integrations.AzureIntegration,
	data *credentials.AzureCredential,
) error {
	reqData := &CreateVaultSecretRequest{
		Data: data,
	}

	return c.postRequest(fmt.Sprintf("/v1/%s", c.getAzureCredentialPath(azIntegration)), reqData, nil)
}

func (c *Client) GetAzureCredential(azIntegration *integrations.AzureIntegration) (*credentials.AzureCredential, error) {
	resp := &GetAzureCredentialResponse{}

	err := c.getRequest(fmt.Sprintf("/v1/%s", c.getAzureCredentialPath(azIntegration)), resp)
	if err != nil {
		return nil, err
	}

	return resp.Data.Data, nil
}

func (c *Client) CreateAzureToken(azIntegration *integrations.AzureIntegration) (string, error) {
	credPath := c.getAzureCredentialPath(azIntegration)
	policyName := fmt.Sprintf("access-%d-azure-%d", azIntegration.ProjectID, azIntegration.ID)

	return c.getToken(credPath, policyName)
}

func (c *Client) getAzureCredentialPath(azIntegration *integrations.AzureIntegration) string {
	return fmt.Sprintf(
		"kv/data/secret/%s/%d/azure/%d",
		c.secretPrefix,
		azIntegration.ProjectID,
		azIntegration.ID,
	)
}

func (c *Client) WriteGitlabCredential(giIntegration *integrations.GitlabIntegration, data *credentials.GitlabCredential) error {
	reqData := &CreateVaultSecretRequest{
		Data: data,
	}

	return c.postRequest(fmt.Sprintf("/v1/%s", c.getGitlabCredentialPath(giIntegration)), reqData, nil)
}

func (c *Client) GetGitlabCredential(
	giIntegration *integrations.GitlabIntegration,
) (*credentials.GitlabCredential, error) {
	resp := &GetGitlabCredentialResponse{}

	err := c.getRequest(fmt.Sprintf("/v1/%s", c.getGitlabCredentialPath(giIntegration)), resp)
	if err != nil {
		return nil, err
	}

	return resp.Data.Data, nil
}

func (c *Client) CreateGitlabToken(giIntegration *integrations.GitlabIntegration) (string, error) {
	panic("not implemented")
}

func (c *Client) getGitlabCredentialPath(giIntegration *integrations.GitlabIntegration) string {
	return fmt.Sprintf(
		"kv/data/secret/%s/%d/gitlab/%d",
		c.secretPrefix,
		giIntegration.ProjectID,
		giIntegration.ID,
	)
}

const readOnlyPolicyTemplate = `path "%s" {
  capabilities = ["read"]
}`

func (c *Client) getToken(credPath, policyName string) (string, error) {
	policy := fmt.Sprintf(readOnlyPolicyTemplate, credPath)

	policyReq := &CreatePolicyRequest{
		Policy: policy,
	}

	err := c.postRequest(fmt.Sprintf("/v1/sys/policy/%s", policyName), policyReq, nil)
	if err != nil {
		return "", err
	}

	tokenReq := &CreateTokenRequest{
		Policies: []string{policyName},
		Meta: Meta{
			User: policyName,
		},
		TTL: "6h",
	}

	tokenResp := &CreateTokenResponse{}

	err = c.postRequest("/v1/auth/token/create", tokenReq, tokenResp)

	if err != nil {
		return "", err
	}

	return tokenResp.Auth.Token, nil
}

func (c *Client) postRequest(path string, data interface{}, dst interface{}) error {
	return c.writeRequest("POST", path, data, dst)
}

func (c *Client) putRequest(path string, data interface{}, dst interface{}) error {
	return c.writeRequest("PUT", path, data, dst)
}

func (c *Client) deleteRequest(path string, data interface{}, dst interface{}) error {
	return c.writeRequest("DELETE", path, data, dst)
}

func (c *Client) getRequest(path string, dst interface{}) error {
	reqURL, err := url.Parse(c.serverURL)
	if err != nil {
		return nil
	}

	reqURL.Path = path

	req, err := http.NewRequest(
		"GET",
		reqURL.String(),
		nil,
	)
	if err != nil {
		return err
	}

	req.Header.Set("Content-Type", "application/json; charset=utf-8")
	req.Header.Set("Accept", "application/json; charset=utf-8")
	req.Header.Set("X-Vault-Token", c.apiKey)

	res, err := c.httpClient.Do(req)
	if err != nil {
		return err
	}

	defer res.Body.Close()

	if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
		resBytes, err := ioutil.ReadAll(res.Body)
		if err != nil {
			return fmt.Errorf("request failed with status code %d, but could not read body (%s)\n", res.StatusCode, err.Error())
		}

		return fmt.Errorf("request failed with status code %d: %s\n", res.StatusCode, string(resBytes))
	}

	if dst != nil {
		return json.NewDecoder(res.Body).Decode(dst)
	}

	return nil
}

func (c *Client) writeRequest(method, path string, data interface{}, dst interface{}) error {
	reqURL, err := url.Parse(c.serverURL)
	if err != nil {
		return nil
	}

	reqURL.Path = path

	var strData []byte

	if data != nil {
		strData, err = json.Marshal(data)

		if err != nil {
			return err
		}
	}

	req, err := http.NewRequest(
		method,
		reqURL.String(),
		strings.NewReader(string(strData)),
	)
	if err != nil {
		return err
	}

	req.Header.Set("Content-Type", "application/json; charset=utf-8")
	req.Header.Set("Accept", "application/json; charset=utf-8")
	req.Header.Set("X-Vault-Token", c.apiKey)

	res, err := c.httpClient.Do(req)
	if err != nil {
		return err
	}

	defer res.Body.Close()

	if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
		resBytes, err := ioutil.ReadAll(res.Body)
		if err != nil {
			return fmt.Errorf("request failed with status code %d, but could not read body (%s)\n", res.StatusCode, err.Error())
		}

		return fmt.Errorf("request failed with status code %d: %s\n", res.StatusCode, string(resBytes))
	}

	if dst != nil {
		return json.NewDecoder(res.Body).Decode(dst)
	}

	return nil
}
