// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package metric

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"time"

	"github.com/harness/gitness/app/services/system"
	"github.com/harness/gitness/app/store"
	"github.com/harness/gitness/job"
	store2 "github.com/harness/gitness/registry/app/store"
	"github.com/harness/gitness/types"
	"github.com/harness/gitness/version"

	"github.com/google/uuid"
)

const jobType = "metric-collector"

type metricData struct {
	IP             string `json:"ip"`
	Hostname       string `json:"hostname"`
	InstallID      string `json:"install_id"`
	Installer      string `json:"installed_by"`
	Installed      string `json:"installed_at"`
	Version        string `json:"version"`
	Users          int64  `json:"user_count"`
	RepoCount      int64  `json:"repo_count"`
	PipelineCount  int64  `json:"pipeline_count"`
	ExecutionCount int64  `json:"execution_count"`
	GitspaceCount  int64  `json:"gitspace_count"`
	RegistryCount  int64  `json:"registry_count"`
	ArtifactCount  int64  `json:"artifact_count"`
}

type Collector struct {
	hostname            string
	enabled             bool
	endpoint            string
	token               string
	userStore           store.PrincipalStore
	repoStore           store.RepoStore
	pipelineStore       store.PipelineStore
	executionStore      store.ExecutionStore
	scheduler           *job.Scheduler
	gitspaceConfigStore store.GitspaceConfigStore
	registryStore       store2.RegistryRepository
	artifactStore       store2.ArtifactRepository
	system              *system.Service
	systemSettings      *system.Settings
}

func (c *Collector) Register(ctx context.Context) error {
	if !c.enabled {
		return nil
	}

	settings, err := c.system.Find(ctx)
	if err != nil {
		return fmt.Errorf("failed to find system settings: %w", err)
	}

	if settings.InstallID == nil || *settings.InstallID == "" {
		id := uuid.New().String()
		settings = &system.Settings{
			InstallID: &id,
		}
		settings, err = c.system.Update(ctx, settings)
		if err != nil {
			return fmt.Errorf("failed to update system settings: %w", err)
		}
	}
	c.systemSettings = settings

	err = c.scheduler.AddRecurring(ctx, jobType, jobType, "0 0 * * *", time.Minute)
	if err != nil {
		return fmt.Errorf("failed to register recurring job for collector: %w", err)
	}

	return nil
}

func (c *Collector) Handle(ctx context.Context, _ string, _ job.ProgressReporter) (string, error) {
	if !c.enabled {
		return "", nil
	}

	// get first available user
	users, err := c.userStore.ListUsers(ctx, &types.UserFilter{
		Page: 1,
		Size: 1,
	})
	if err != nil {
		return "", err
	}
	if len(users) == 0 {
		return "", nil
	}

	// total users in the system
	totalUsers, err := c.userStore.CountUsers(ctx, &types.UserFilter{})
	if err != nil {
		return "", fmt.Errorf("failed to get users count: %w", err)
	}

	// total repos in the system
	totalRepos, err := c.repoStore.Count(ctx, 0, &types.RepoFilter{})
	if err != nil {
		return "", fmt.Errorf("failed to get repositories count: %w", err)
	}

	// total pipelines in the system
	totalPipelines, err := c.pipelineStore.Count(ctx, 0, &types.ListPipelinesFilter{})
	if err != nil {
		return "", fmt.Errorf("failed to get pipelines count: %w", err)
	}

	// total executions in the system
	totalExecutions, err := c.executionStore.Count(ctx, 0)
	if err != nil {
		return "", fmt.Errorf("failed to get executions count: %w", err)
	}

	// total gitspaces (configs) in the system
	totalGitspaces, err := c.gitspaceConfigStore.Count(ctx, &types.GitspaceFilter{})
	if err != nil {
		return "", fmt.Errorf("failed to get gitspace count: %w", err)
	}

	totalRegistries, err := c.registryStore.Count(ctx)
	if err != nil {
		return "", fmt.Errorf("failed to get registries count: %w", err)
	}

	totalArtifacts, err := c.artifactStore.Count(ctx)
	if err != nil {
		return "", fmt.Errorf("failed to get artifacts count: %w", err)
	}

	data := metricData{
		Hostname:       c.hostname,
		InstallID:      *c.systemSettings.InstallID,
		Installer:      users[0].Email,
		Installed:      time.UnixMilli(users[0].Created).Format("2006-01-02 15:04:05"),
		Version:        version.Version.String(),
		Users:          totalUsers,
		RepoCount:      totalRepos,
		PipelineCount:  totalPipelines,
		ExecutionCount: totalExecutions,
		GitspaceCount:  totalGitspaces,
		RegistryCount:  totalRegistries,
		ArtifactCount:  totalArtifacts,
	}

	buf := new(bytes.Buffer)
	err = json.NewEncoder(buf).Encode(data)
	if err != nil {
		return "", fmt.Errorf("failed to encode metric data: %w", err)
	}

	endpoint := fmt.Sprintf("%s?api_key=%s", c.endpoint, c.token)
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, buf)
	if err != nil {
		return "", fmt.Errorf("failed to create a request for metric data to endpoint %s: %w", endpoint, err)
	}
	req.Header.Add("Accept", "application/json")
	req.Header.Add("Content-Type", "application/json")

	res, err := httpClient.Do(req)
	if err != nil {
		return "", fmt.Errorf("failed to send metric data to endpoint %s: %w", endpoint, err)
	}

	res.Body.Close()

	return res.Status, nil
}

// httpClient should be used for HTTP requests. It
// is configured with a timeout for reliability.
var httpClient = &http.Client{
	Transport: &http.Transport{
		Proxy:               http.ProxyFromEnvironment,
		TLSHandshakeTimeout: 30 * time.Second,
		DisableKeepAlives:   true,
	},
	Timeout: 1 * time.Minute,
}
