package pia

import (
	"bytes"
	"context"
	"fmt"
	"mime/multipart"
	"net/http"
	"strings"
	"time"
)

type AuthClient struct {
	Endpoint   string
	HTTPClient *http.Client
	MaxBody    int64
	UserAgent  string
	Now        func() time.Time
}

func NewAuthClient(endpoint string) *AuthClient {
	return &AuthClient{
		Endpoint:   endpoint,
		MaxBody:    DefaultMaxResponseBody,
		UserAgent:  DefaultUserAgent,
		Now:        time.Now,
		HTTPClient: &http.Client{Timeout: DefaultRequestTimeout, CheckRedirect: noRedirect},
	}
}

func (c *AuthClient) Authenticate(ctx context.Context, username string, password []byte) (Token, error) {
	username = strings.TrimSpace(username)
	if !validSecret([]byte(username), 1, 256) || !validSecret(password, 1, 1024) {
		return Token{}, NewError(CodeInvalidCredentials, "Enter a valid PIA username and password.")
	}
	var body bytes.Buffer
	writer := multipart.NewWriter(&body)
	if err := writer.WriteField("username", username); err != nil {
		return Token{}, WrapError(CodeAuthenticationUnavailable, "Could not prepare the authentication request.", err)
	}
	if err := writer.WriteField("password", string(password)); err != nil {
		return Token{}, WrapError(CodeAuthenticationUnavailable, "Could not prepare the authentication request.", err)
	}
	if err := writer.Close(); err != nil {
		return Token{}, WrapError(CodeAuthenticationUnavailable, "Could not prepare the authentication request.", err)
	}

	request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.Endpoint, &body)
	if err != nil {
		return Token{}, WrapError(CodeAuthenticationUnavailable, "The authentication endpoint is invalid.", err)
	}
	request.Header.Set("Content-Type", writer.FormDataContentType())
	request.Header.Set("Accept", "application/json")
	request.Header.Set("User-Agent", c.UserAgent)
	response, err := c.HTTPClient.Do(request)
	if err != nil {
		return Token{}, classifyNetworkError(ctx, CodeAuthenticationUnavailable, "PIA authentication could not be reached.", err)
	}
	defer response.Body.Close()
	if response.StatusCode == http.StatusUnauthorized || response.StatusCode == http.StatusForbidden {
		return Token{}, NewError(CodeInvalidCredentials, "The PIA username or password was rejected.")
	}
	if response.StatusCode != http.StatusOK {
		return Token{}, NewError(CodeAuthenticationUnavailable, fmt.Sprintf("PIA authentication returned HTTP %d.", response.StatusCode))
	}
	if !expectedContentType(response.Header.Get("Content-Type"), "application/json") {
		return Token{}, NewError(CodeAuthenticationUnavailable, "PIA authentication returned an unexpected content type.")
	}
	raw, err := readLimitedBody(response.Body, c.MaxBody)
	if err != nil {
		return Token{}, WrapError(CodeAuthenticationUnavailable, "PIA authentication returned an invalid response.", err)
	}
	var payload struct {
		Token string `json:"token"`
	}
	if err := decodeSingleJSON(raw, &payload); err != nil || !validSecret([]byte(payload.Token), 16, 4096) {
		return Token{}, NewError(CodeAuthenticationUnavailable, "PIA authentication returned an invalid token response.")
	}
	now := time.Now
	if c.Now != nil {
		now = c.Now
	}
	return Token{Value: []byte(payload.Token), ExpiresAt: now().Add(DefaultTokenTTL)}, nil
}
