package connector

import (
	"encoding/json"
	"fmt"
	"html/template"
	"net/http"
	"net/url"
	"path"

	chttp "github.com/coreos/go-oidc/http"
	"github.com/coreos/go-oidc/oauth2"
	"github.com/coreos/go-oidc/oidc"
)

const (
	BitbucketConnectorType = "bitbucket"
	bitbucketAuthURL       = "https://bitbucket.org/site/oauth2/authorize"
	bitbucketTokenURL      = "https://bitbucket.org/site/oauth2/access_token"
	bitbucketAPIUserURL    = "https://bitbucket.org/api/2.0/user"
	bitbucketAPIEmailURL   = "https://api.bitbucket.org/2.0/user/emails"
)

func init() {
	RegisterConnectorConfigType(BitbucketConnectorType, func() ConnectorConfig { return &BitbucketConnectorConfig{} })
}

type BitbucketConnectorConfig struct {
	ID           string `json:"id"`
	ClientID     string `json:"clientID"`
	ClientSecret string `json:"clientSecret"`
}

func (cfg *BitbucketConnectorConfig) ConnectorID() string {
	return cfg.ID
}

func (cfg *BitbucketConnectorConfig) ConnectorType() string {
	return BitbucketConnectorType
}

func (cfg *BitbucketConnectorConfig) Connector(ns url.URL, lf oidc.LoginFunc, tpls *template.Template) (Connector, error) {
	ns.Path = path.Join(ns.Path, httpPathCallback)
	oauth2Conn, err := newBitbucketConnector(cfg.ClientID, cfg.ClientSecret, ns.String())
	if err != nil {
		return nil, err
	}
	return &OAuth2Connector{
		id:        cfg.ID,
		loginFunc: lf,
		cbURL:     ns,
		conn:      oauth2Conn,
	}, nil
}

type bitbucketOAuth2Connector struct {
	clientID     string
	clientSecret string
	client       *oauth2.Client
}

func newBitbucketConnector(clientID, clientSecret, cbURL string) (oauth2Connector, error) {
	config := oauth2.Config{
		Credentials: oauth2.ClientCredentials{ID: clientID, Secret: clientSecret},
		AuthURL:     bitbucketAuthURL,
		TokenURL:    bitbucketTokenURL,
		AuthMethod:  oauth2.AuthMethodClientSecretPost,
		RedirectURL: cbURL,
	}

	cli, err := oauth2.NewClient(http.DefaultClient, config)
	if err != nil {
		return nil, err
	}

	return &bitbucketOAuth2Connector{
		clientID:     clientID,
		clientSecret: clientSecret,
		client:       cli,
	}, nil
}

func (c *bitbucketOAuth2Connector) Client() *oauth2.Client {
	return c.client
}

func (c *bitbucketOAuth2Connector) Identity(cli chttp.Client) (oidc.Identity, error) {
	var user struct {
		UUID        string `json:"uuid"`
		Username    string `json:"username"`
		DisplayName string `json:"display_name"`
	}
	if err := getAndDecode(cli, bitbucketAPIUserURL, &user); err != nil {
		return oidc.Identity{}, fmt.Errorf("getting user info: %v", err)
	}

	name := user.DisplayName
	if name == "" {
		name = user.Username
	}

	var emails struct {
		Values []struct {
			Email     string `json:"email"`
			Confirmed bool   `json:"is_confirmed"`
			Primary   bool   `json:"is_primary"`
		} `json:"values"`
	}
	if err := getAndDecode(cli, bitbucketAPIEmailURL, &emails); err != nil {
		return oidc.Identity{}, fmt.Errorf("getting user email: %v", err)
	}
	email := ""
	for _, val := range emails.Values {
		if !val.Confirmed {
			continue
		}
		if email == "" || val.Primary {
			email = val.Email
		}
		if val.Primary {
			break
		}
	}

	return oidc.Identity{
		ID:    user.UUID,
		Name:  name,
		Email: email,
	}, nil
}

func getAndDecode(cli chttp.Client, url string, v interface{}) error {
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return err
	}
	resp, err := cli.Do(req)
	if err != nil {
		return fmt.Errorf("get: %v", err)
	}
	defer resp.Body.Close()
	switch {
	case resp.StatusCode >= 400 && resp.StatusCode < 500:
		return oauth2.NewError(oauth2.ErrorAccessDenied)
	case resp.StatusCode == http.StatusOK:
	default:
		return fmt.Errorf("unexpected status from providor %s", resp.Status)
	}
	if err := json.NewDecoder(resp.Body).Decode(v); err != nil {
		return fmt.Errorf("decode body: %v", err)
	}
	return nil
}

func (c *bitbucketOAuth2Connector) Healthy() error {
	return nil
}

func (c *bitbucketOAuth2Connector) TrustedEmailProvider() bool {
	return false
}