/*
 * Copyright © 2019-2020 Musing Studio LLC.
 *
 * This file is part of WriteFreely.
 *
 * WriteFreely is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License, included
 * in the LICENSE file in this source code package.
 */

package writefreely

import (
	"context"
	"errors"
	"github.com/gosimple/slug"
	"net/http"
	"net/url"
	"strings"
)

type slackOauthClient struct {
	ClientID         string
	ClientSecret     string
	TeamID           string
	CallbackLocation string
	HttpClient       HttpClient
}

type slackExchangeResponse struct {
	OK          bool   `json:"ok"`
	AccessToken string `json:"access_token"`
	Scope       string `json:"scope"`
	TeamName    string `json:"team_name"`
	TeamID      string `json:"team_id"`
	Error       string `json:"error"`
}

type slackIdentity struct {
	Name  string `json:"name"`
	ID    string `json:"id"`
	Email string `json:"email"`
}

type slackTeam struct {
	Name string `json:"name"`
	ID   string `json:"id"`
}

type slackUserIdentityResponse struct {
	OK    bool          `json:"ok"`
	User  slackIdentity `json:"user"`
	Team  slackTeam     `json:"team"`
	Error string        `json:"error"`
}

const (
	slackAuthLocation     = "https://slack.com/oauth/authorize"
	slackExchangeLocation = "https://slack.com/api/oauth.access"
	slackIdentityLocation = "https://slack.com/api/users.identity"
)

var _ oauthClient = slackOauthClient{}

func (c slackOauthClient) GetProvider() string {
	return "slack"
}

func (c slackOauthClient) GetClientID() string {
	return c.ClientID
}

func (c slackOauthClient) GetCallbackLocation() string {
	return c.CallbackLocation
}

func (c slackOauthClient) buildLoginURL(state string) (string, error) {
	u, err := url.Parse(slackAuthLocation)
	if err != nil {
		return "", err
	}
	q := u.Query()
	q.Set("client_id", c.ClientID)
	q.Set("scope", "identity.basic identity.email identity.team")
	q.Set("redirect_uri", c.CallbackLocation)
	q.Set("state", state)

	// If this param is not set, the user can select which team they
	// authenticate through and then we'd have to match the configured team
	// against the profile get. That is extra work in the post-auth phase
	// that we don't want to do.
	q.Set("team", c.TeamID)

	// The Slack OAuth docs don't explicitly list this one, but it is part of
	// the spec, so we include it anyway.
	q.Set("response_type", "code")
	u.RawQuery = q.Encode()
	return u.String(), nil
}

func (c slackOauthClient) exchangeOauthCode(ctx context.Context, code string) (*TokenResponse, error) {
	form := url.Values{}
	// The oauth.access documentation doesn't explicitly mention this
	// parameter, but it is part of the spec, so we include it anyway.
	// https://api.slack.com/methods/oauth.access
	form.Add("grant_type", "authorization_code")
	form.Add("redirect_uri", c.CallbackLocation)
	form.Add("code", code)
	req, err := http.NewRequest("POST", slackExchangeLocation, strings.NewReader(form.Encode()))
	if err != nil {
		return nil, err
	}
	req.WithContext(ctx)
	req.Header.Set("User-Agent", ServerUserAgent(""))
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.SetBasicAuth(c.ClientID, c.ClientSecret)

	resp, err := c.HttpClient.Do(req)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode != http.StatusOK {
		return nil, errors.New("unable to exchange code for access token")
	}

	var tokenResponse slackExchangeResponse
	if err := limitedJsonUnmarshal(resp.Body, tokenRequestMaxLen, &tokenResponse); err != nil {
		return nil, err
	}
	if !tokenResponse.OK {
		return nil, errors.New(tokenResponse.Error)
	}
	return tokenResponse.TokenResponse(), nil
}

func (c slackOauthClient) inspectOauthAccessToken(ctx context.Context, accessToken string) (*InspectResponse, error) {
	req, err := http.NewRequest("GET", slackIdentityLocation, nil)
	if err != nil {
		return nil, err
	}
	req.WithContext(ctx)
	req.Header.Set("User-Agent", ServerUserAgent(""))
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Authorization", "Bearer "+accessToken)

	resp, err := c.HttpClient.Do(req)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode != http.StatusOK {
		return nil, errors.New("unable to inspect access token")
	}

	var inspectResponse slackUserIdentityResponse
	if err := limitedJsonUnmarshal(resp.Body, infoRequestMaxLen, &inspectResponse); err != nil {
		return nil, err
	}
	if !inspectResponse.OK {
		return nil, errors.New(inspectResponse.Error)
	}
	return inspectResponse.InspectResponse(), nil
}

func (resp slackUserIdentityResponse) InspectResponse() *InspectResponse {
	return &InspectResponse{
		UserID:      resp.User.ID,
		Username:    slug.Make(resp.User.Name),
		DisplayName: resp.User.Name,
		Email:       resp.User.Email,
	}
}

func (resp slackExchangeResponse) TokenResponse() *TokenResponse {
	return &TokenResponse{
		AccessToken: resp.AccessToken,
	}
}
