package metrics

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"regexp"
	"strconv"
	"strings"

	"github.com/bazelbuild/continuous-integration/metrics/clients"
	"github.com/bazelbuild/continuous-integration/metrics/data"
)

type Flakiness struct {
	client    *clients.CloudStorageClient
	columns   []Column
	gcsBucket string
	gcsSuffix string
	pipelines []*data.PipelineID
}

func (f *Flakiness) Name() string {
	return "flakiness"
}

func (f *Flakiness) Columns() []Column {
	return f.columns
}

func (f *Flakiness) Collect() (data.DataSet, error) {
	result := data.CreateDataSet(GetColumnNames(f.columns))
	for _, pipeline := range f.pipelines {
		gcsPath := f.gcsSuffix + pipeline.Slug
		iter, err := f.client.ReadAllFiles(f.gcsBucket, gcsPath)
		if err != nil {
			return nil, err
		}
		if !iter.HasNext() {
			log.Printf("There is no flakiness data for pipeline %s in GCS location %s/%s", pipeline, f.gcsBucket, gcsPath)
			continue
		}

		for iter.HasNext() {
			fileName, content, err := iter.Get()
			if err != nil {
				return nil, fmt.Errorf("Failed to read flakiness data for pipeline %s: %v", pipeline, err)
			}

			build, err := getBuildNumber(fileName)
			if err != nil {
				return nil, fmt.Errorf("Invalid flaky data file %s in %s/%s: %v", fileName, f.gcsBucket, gcsPath, err)
			}

			messages, err := getFlakyMessages(content)
			if err != nil {
				return nil, fmt.Errorf("Failed to parse flaky data file %s in %s/%s: %v", fileName, f.gcsBucket, gcsPath, err)
			}

			for _, msg := range messages {
				err = result.AddRow(pipeline.Org, pipeline.Slug, build, msg.ID.Summary.Label, len(msg.Summary.Passed), len(msg.Summary.Failed))
				if err != nil {
					return nil, fmt.Errorf("Failed to add flakiness data for build #%d of pipeline %s: %v", build, pipeline, err)
				}
			}
		}
	}
	return result, nil
}

func getFlakyMessages(content []byte) ([]message, error) {
	messages := make([]message, 0)
	for _, line := range bytes.Split(content, []byte("\n")) {
		if len(line) == 0 {
			continue
		}

		var msg message
		err := json.Unmarshal([]byte(line), &msg)
		if err != nil {
			return nil, fmt.Errorf("JSON parse error: %v. Line:\n%s", err, line)
		}
		if msg.Summary.Status == "FLAKY" {
			messages = append(messages, msg)
		}
	}
	return messages, nil
}

func getBuildNumber(fileName string) (int, error) {
	re := regexp.MustCompile(`(\d+).json$`)
	matches := re.FindStringSubmatch(fileName)
	if len(matches) == 2 {
		if build, err := strconv.Atoi(matches[1]); err == nil {
			return build, nil
		}
	}
	return 0, fmt.Errorf("Flakiness data file names must be 'some_path/<build_number.json>'. Invalid given value: '%s'", fileName)
}

type message struct {
	ID struct {
		Summary struct {
			Label string `json:"label"`
		} `json:"testSummary"`
	}
	Summary struct {
		Passed []struct{ uri string } `json:"passed"`
		Failed []struct{ uri string } `json:"failed"`
		Status string                 `json:"overallStatus"`
	} `json:"testSummary"`
}

// CREATE TABLE flakiness (org VARCHAR(255), pipeline VARCHAR(255), build INT, target VARCHAR(255), passed_count INT, failed_count INT, PRIMARY KEY(org, pipeline, build, target));
func CreateFlakiness(client *clients.CloudStorageClient, gcsBucket, gcsBasePath string, pipelines ...*data.PipelineID) *Flakiness {
	columns := []Column{Column{"org", true}, Column{"pipeline", true}, Column{"build", true}, Column{"target", true}, Column{"passed_count", false}, Column{"failed_count", false}}
	gcsSuffix := gcsBasePath
	if !strings.HasSuffix(gcsBasePath, "/") {
		gcsSuffix = gcsBasePath + "/"
	}
	return &Flakiness{client: client, columns: columns, gcsBucket: gcsBucket, gcsSuffix: gcsSuffix, pipelines: pipelines}
}
