package ingest

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/aws/aws-sdk-go/service/s3/s3manager"
	"github.com/aws/aws-sdk-go/service/secretsmanager"
)

const dateLayout = "2006-01-02"

var httpClient = &http.Client{}

type spotifyToken struct {
	AccessToken string `json:"access_token"`
	ExpiresIn   int    `json:"expires_in"`
}

type spotifyCredentials struct {
	ClientID     string `json:"client_id"`
	ClientSecret string `json:"client_secret"`
}

func getSpotifyAccessToken(clientID string, clientSecret string) (string, error) {
	reqURL := "https://accounts.spotify.com/api/token"
	reqBody := strings.NewReader("grant_type=client_credentials")

	req, err := http.NewRequest("POST", reqURL, reqBody)
	if err != nil {
		return "", err
	}
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
	req.SetBasicAuth(clientID, clientSecret)

	resp, err := httpClient.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	respBody, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	var t spotifyToken

	err = json.Unmarshal(respBody, &t)
	if err != nil {
		return "", err
	}

	return t.AccessToken, nil
}

func getSpotifyCredentials(spotifyCredentialsSecretID string) (string, string, error) {
	svc := secretsmanager.New(session.New(&aws.Config{Region: aws.String("us-east-1")}))
	input := &secretsmanager.GetSecretValueInput{
		SecretId: aws.String(spotifyCredentialsSecretID),
	}

	result, err := svc.GetSecretValue(input)
	if err != nil {
		return "", "", err
	}

	var credentials spotifyCredentials
	err = json.Unmarshal([]byte(*result.SecretString), &credentials)
	if err != nil {
		return "", "", err
	}

	return credentials.ClientID, credentials.ClientSecret, nil
}

func getS3ObjectSize(s3Svc *s3.S3, bucket string, key string) (int64, error) {
	input := &s3.HeadObjectInput{
		Bucket: aws.String(bucket),
		Key:    aws.String(key),
	}

	result, err := s3Svc.HeadObject(input)
	if err != nil {
		if aerr, ok := err.(awserr.Error); ok {
			switch aerr.Code() {
			case "NotFound":
				return 0, nil
			default:
				return 0, err
			}
		} else {
			return 0, err
		}
	}

	return *result.ContentLength, nil
}

func deleteS3Object(s3Svc *s3.S3, bucket string, key string) error {
	input := &s3.DeleteObjectInput{
		Bucket: aws.String(bucket),
		Key:    aws.String(key),
	}

	_, err := s3Svc.DeleteObject(input)
	if err != nil {
		return err
	}

	return nil
}

func s3ObjectExists(s3Svc *s3.S3, bucket string, key string) (bool, error) {
	input := &s3.HeadObjectInput{
		Bucket: aws.String(bucket),
		Key:    aws.String(key),
	}

	_, err := s3Svc.HeadObject(input)
	if err != nil {
		if aerr, ok := err.(awserr.Error); ok {
			switch aerr.Code() {
			case "NotFound":
				return false, nil
			default:
				return false, err
			}
		} else {
			return false, err
		}
	}
	return true, nil
}

func getDateRange(days int) []string {
	now := time.Now()

	dates := []string{now.Format(dateLayout)}

	for i := 0; i < days; i++ {
		now = now.Add(time.Duration(-24) * time.Hour)
		dates = append(dates, now.Format((dateLayout)))
	}

	return dates
}

func download(date string, accessToken string, s3Bucket string, s3Prefix string, s3Sess *session.Session, s3Svc *s3.S3) error {
	parts := strings.Split(date, "-")
	if len(parts) < 3 {
		return errors.New("incorrect date")
	}
	year := parts[0]
	month := parts[1]
	day := parts[2]

	reqURL := "https://provider-api.spotify.com/v1/analytics/partneremails/" + year + "/" + month + "/" + day

	req, err := http.NewRequest("GET", reqURL, nil)
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", "Bearer "+accessToken)

	resp, err := httpClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		fmt.Println(date, "not available from Spotify, status:", resp.Status)
		return nil
	}

	respContentType := resp.Header.Get("Content-Type")

	if respContentType != "application/x-gzip" && respContentType != "application/octet-stream" {
		fmt.Println(date, "unexpected Content-Type", resp.Header.Get("Content-Type"))
		return nil
	}

	fmt.Println(date, "start download/upload")

	contentLength := resp.ContentLength
	s3Key := s3Prefix + date + ".gz"

	uploader := s3manager.NewUploader(s3Sess)
	_, err = uploader.Upload(&s3manager.UploadInput{
		Bucket:      aws.String(s3Bucket),
		Key:         aws.String(s3Key),
		Body:        resp.Body,
		ContentType: aws.String("application/gzip"),
		ACL:         aws.String("bucket-owner-full-control"),
	})

	fmt.Println(date, "finish download/upload")

	fmt.Println(date, "validating uploaded object to S3")

	expectedFileSize := contentLength
	actualFileSize, err := getS3ObjectSize(s3Svc, s3Bucket, s3Key)
	if err != nil {
		return err
	}

	if actualFileSize != expectedFileSize {
		fmt.Println(date, "bytes received does not match bytes uploaded to S3")
		fmt.Println(date, "expected size", expectedFileSize)
		fmt.Println(date, "actual size", actualFileSize)
		fmt.Println(date, "deleting from S3")
		err = deleteS3Object(s3Svc, s3Bucket, s3Key)
		if err != nil {
			return err
		}
	} else {
		fmt.Println(date, "validation completed successfully")
	}

	return nil
}

// Handler is the AWS Lambda handler
func Handler() error {
	s3Bucket := os.Getenv("S3_BUCKET")
	s3Region := os.Getenv("S3_REGION")
	s3Prefix := os.Getenv("S3_PREFIX")
	daysToCheckEnv := os.Getenv("DAYS_TO_CHECK")
	spotifyCredentialsSecretID := os.Getenv("SPOTIFY_CREDENTIALS_SECRET_ID")

	if s3Bucket == "" {
		return errors.New("S3_BUCKET env var not set")
	}
	if s3Prefix == "" {
		return errors.New("S3_PREFIX env var not set")
	}
	if s3Region == "" {
		return errors.New("S3_REGION env var not set")
	}
	if daysToCheckEnv == "" {
		return errors.New("DAYS_TO_CHECK env var not set")
	}
	if spotifyCredentialsSecretID == "" {
		return errors.New("SPOTIFY_CREDENTIALS_SECRET_ID env var not set")
	}

	s3Sess := session.New(&aws.Config{Region: aws.String(s3Region)})
	s3Svc := s3.New(s3Sess)

	daysToCheck, err := strconv.Atoi(daysToCheckEnv)
	if err != nil {
		return err
	}

	fmt.Println("request Spotify Credentials from Secrets Manager")
	clientID, clientSecret, err := getSpotifyCredentials(spotifyCredentialsSecretID)
	if err != nil {
		return err
	}
	fmt.Println("received Spotify Credentials from Secrets Manager")

	fmt.Println("request access token from Spotify")
	token, err := getSpotifyAccessToken(clientID, clientSecret)
	if err != nil {
		return err
	}
	fmt.Println("received access token from Spotify")

	for _, date := range getDateRange(daysToCheck) {
		exists, err := s3ObjectExists(s3Svc, s3Bucket, s3Prefix+date+".gz")
		if err != nil {
			return err
		}

		if exists == true {
			fmt.Println(date, "already downloaded")
			continue
		}

		err = download(date, token, s3Bucket, s3Prefix, s3Sess, s3Svc)
		if err != nil {
			return err
		}
	}

	return nil
}
