/*
Copyright 2024.

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 integrationtask

import (
	"context"
	"fmt"

	o11yv1 "github.com/tidbcloud/observability/api/regionalserver/resourceworker/v1"
	"github.com/tidbcloud/observability/pkg/common/constants"
	"github.com/tidbcloud/observability/pkg/common/deploy"
	"github.com/tidbcloud/observability/pkg/resourceworker/controllers/integrationtask/v2/task"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	"sigs.k8s.io/controller-runtime/pkg/log"
)

const (
	awsRoleArnTemplate      = "arn:aws:iam::%s:role/%s"
	aliCloudRoleArnTemplate = "acs:ram::%s:role/%s"
	// tenantDataPathTmpl aggregates all clusters under the tenant path; cluster_id
	// becomes a data column instead of a path segment.
	tenantDataPathTmpl = "%s/log_data/data/%s"
)

// updateCreate processes task results during creation phase and updates IntegrationTask status.
// Sets phase to ACTIVE and populates DestinationInfo when all tasks complete successfully.
func updateCreate(ctx context.Context, t *task.Context, results []deploy.Result) (res ctrl.Result, err error) {
	var retriableErr error
	res, err, retriableErr = deploy.MinRequeue(results)
	if err == nil && retriableErr == nil {
		// Do not overwrite terminal phases (COMPLETED/FAILED) back to ACTIVE
		if t.Task.Status.Phase != constants.ACTIVE &&
			t.Task.Status.Phase != constants.COMPLETED &&
			t.Task.Status.Phase != constants.FAILED {
			log.FromContext(ctx).Info("marking integration task as active", "task", t.Task.Name)
			t.Task.Status.Phase = constants.ACTIVE
		}

		// Populate DestinationInfo after successful resource reconciliation.
		if err := populateDestinationInfo(ctx, t); err != nil {
			log.FromContext(ctx).Error(err, "failed to populate DestinationInfo")
			// Continue anyway - this is informational data
		}
	}

	if err != nil {
		log.FromContext(ctx).Error(err, "failed in MinRequeue for integration task")
	}

	if retriableErr != nil {
		log.FromContext(ctx).Info("retriable error", "error", retriableErr)
	}

	if t.IsTaskChanged() {
		return res, update(ctx, t.Client, t.Task)
	}

	return res, err
}

// updateDelete processes task results during deletion phase and deletes IntegrationTask CR on success.
// Sets phase to DELETING and removes the CR when all tasks complete successfully.
func updateDelete(ctx context.Context, t *task.Context, results []deploy.Result) (res ctrl.Result, err error) {
	var deleted bool
	var retriableErr error
	res, err, retriableErr = deploy.MinRequeue(results)
	if err == nil && retriableErr == nil {
		deleted = true
	}

	t.Task.Status.Phase = constants.DELETING

	if err != nil {
		log.FromContext(ctx).Error(err, "failed in MinRequeue for integration task")
	}

	if retriableErr != nil {
		log.FromContext(ctx).Info("retriable error", "error", retriableErr)
	}

	if deleted {
		controllerutil.RemoveFinalizer(t.Task, task.Finalizer)
		// Removing the finalizer garbage-collects the object, so only persist the
		// object update. A follow-up status update would race with deletion and
		// fail with NotFound.
		if e := t.Client.Update(ctx, t.Task); e != nil {
			return res, client.IgnoreNotFound(e)
		}
		return res, err
	}
	if e := update(ctx, t.Client, t.Task); e != nil {
		return res, e
	}

	return res, err
}

// update performs a two-phase update: first update the object, then update the status subresource.
// This ensures status changes are persisted correctly via the status subresource API.
func update(ctx context.Context, cli client.Client, task *o11yv1.IntegrationTask) error {
	// Manually preserve status since IntegrationTaskStatus doesn't have DeepCopy
	taskStatus := task.Status
	if err := cli.Update(ctx, task); err != nil {
		return err
	}
	task.Status = taskStatus
	return cli.Status().Update(ctx, task)
}

// populateDestinationInfo populates the DestinationInfo fields by fetching IAM role ARN and building S3 path.
func populateDestinationInfo(ctx context.Context, t *task.Context) error {
	if t.Task.Labels[constants.LabelInternal] == "true" {
		t.Task.Status.DestinationInfo = o11yv1.DestinationInfo{
			AccessPoint: fmt.Sprintf("s3://%s", t.Task.Spec.DestinationBucketName),
		}
		log.FromContext(ctx).Info("populated DestinationInfo",
			"accessPoint", t.Task.Status.DestinationInfo.AccessPoint,
			"assumeRoleARN", t.Task.Status.DestinationInfo.AssumeRoleARN)
		return nil
	}

	// Build the vendor-specific object storage path. Tenant scope aggregates all
	// clusters under the tenant path (cluster_id is a data column, not a path
	// segment); cluster scope keeps the per-cluster path.
	var dataPath string
	if t.Task.Spec.Scope == o11yv1.ScopeTenant {
		dataPath = fmt.Sprintf(tenantDataPathTmpl,
			t.Task.Spec.DestinationBucketName,
			t.Task.Labels[constants.LabelTenantID])
	} else {
		dataPath = buildClusterDataPath(
			t.Task.Spec.DestinationBucketName,
			t.Task.Labels[constants.LabelTenantID],
			t.Task.Labels[constants.LabelProjectID],
			t.Task.Spec.ClusterID)
	}

	destinationInfo := o11yv1.DestinationInfo{}
	alias := t.Task.Labels[constants.LabelAlias]
	switch t.ServiceInfo.Vendor {
	case "aws":
		destinationInfo.AccessPoint = fmt.Sprintf("s3://%s", dataPath)
		if alias == "" {
			return fmt.Errorf("missing %s label for aws integration task", constants.LabelAlias)
		}
		if t.ServiceInfo.AccountInfo.Account == "" {
			return fmt.Errorf("missing aws account info for integration task")
		}
		destinationInfo.AssumeRoleARN = fmt.Sprintf(awsRoleArnTemplate, t.ServiceInfo.AccountInfo.Account, alias)
	case "alicloud":
		destinationInfo.AccessPoint = fmt.Sprintf("oss://%s", dataPath)
		if alias == "" {
			return fmt.Errorf("missing %s label for alicloud integration task", constants.LabelAlias)
		}
		if t.ServiceInfo.AccountInfo.Account == "" {
			return fmt.Errorf("missing alicloud account info for integration task")
		}
		destinationInfo.AssumeRoleARN = fmt.Sprintf(aliCloudRoleArnTemplate, t.ServiceInfo.AccountInfo.Account, alias)
	default:
		destinationInfo.AccessPoint = fmt.Sprintf("s3://%s", dataPath)
	}

	t.Task.Status.DestinationInfo = destinationInfo

	log.FromContext(ctx).Info("populated DestinationInfo",
		"accessPoint", destinationInfo.AccessPoint,
		"assumeRoleARN", destinationInfo.AssumeRoleARN)

	return nil
}

func buildClusterDataPath(bucketName, tenantID, projectID, clusterID string) string {
	if projectID == "" {
		return fmt.Sprintf("%s/log_data/data/%s/%s", bucketName, tenantID, clusterID)
	}
	return fmt.Sprintf(clusterDataPathTmpl, bucketName, tenantID, projectID, clusterID)
}
