package tidbtools

import (
	"strconv"

	"github.com/tidbcloud/dataflow-service/idl/pbgen/proto/dataflow"
	tidbtoolsapi "github.com/tidbcloud/devops/pkg/service/tidbtools/api"
	nextgenglobal "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
)

// Restore mirrors `Restore` in FE types.
type Restore struct {
	ID               string        `json:"id"`
	Name             string        `json:"name"`
	Mode             RestoreMode   `json:"mode,omitempty" enums:"snapshot,pitr"`
	SourceBackupName string        `json:"source_backup_name"`
	BackupID         string        `json:"backup_id,omitempty"`
	LogBackupID      string        `json:"log_backup_id,omitempty"`
	RestoredTS       string        `json:"restored_ts,omitempty"`
	RestoredAt       int64         `json:"restored_at,omitempty"`
	TargetClusterID  string        `json:"target_cluster_id"`
	RestoreRegion    string        `json:"restore_region,omitempty"`
	Status           RestoreStatus `json:"status" enums:"pending,running,succeeded,failed,canceled,unknown"`
	ErrorMsg         string        `json:"error_msg,omitempty"`
	StartedAt        int64         `json:"started_at"`
	FinishedAt       int64         `json:"finished_at,omitempty"`
	ProgressPercent  int32         `json:"progress_percent"`
}

// ListRestoresResp is the response body for `GET .../restores` (Dedicated).
type ListRestoresResp struct {
	Items []*Restore `json:"items"`
	Total int64      `json:"total"`
}

// ToListPremiumRestoresResp exposes the Premium restore status associated
// with the current target cluster. Premium restore creates a new TiDB
// instance, so unlike Dedicated there is no source-cluster restore-job list.
func ToListPremiumRestoresResp(clusterID string, pb *nextgenglobal.RestoreStatusResponse) *ListRestoresResp {
	out := &ListRestoresResp{Items: []*Restore{}}
	if pb == nil {
		return out
	}
	restore := &Restore{
		ID:              clusterID,
		Name:            "restore-" + clusterID,
		TargetClusterID: clusterID,
		Status:          premiumRestoreStatus(pb.GetState()),
		ProgressPercent: pb.GetProgress(),
	}
	if restore.Status == RestoreStatusFailed {
		restore.ErrorMsg = pb.GetMessage()
	}
	out.Items = append(out.Items, restore)
	out.Total = 1
	return out
}

// RestoreErrorDetail carries error info from RestoreProgressResp.restore_error.
type RestoreErrorDetail struct {
	Message   string `json:"message"`
	Retryable *bool  `json:"retryable,omitempty"`
}

// RestoreStepProgress carries individual step data from RestoreProgressResp.steps.
type RestoreStepProgress struct {
	StepName string  `json:"step_name"`
	Progress float64 `json:"progress"`
}

// RestoreBackupInfo identifies the recovery point used by a restore. Premium
// always exposes the backup ID for snapshot restores; the remaining fields are
// included when the source backup is still available from ng-global.
type RestoreBackupInfo struct {
	ID         string `json:"id"`
	Name       string `json:"name,omitempty"`
	TSO        string `json:"tso,omitempty"`
	Type       string `json:"type,omitempty"`
	Status     string `json:"status,omitempty"`
	CreatedAt  int64  `json:"created_at,omitempty"`
	FinishedAt int64  `json:"finished_at,omitempty"`
	SizeBytes  int64  `json:"size_bytes,omitempty"`
	Region     string `json:"region,omitempty"`
}

type RestoreAuditInfo struct {
	RestoreMode string `json:"restore_mode,omitempty"`
	PointInTime int64  `json:"point_in_time,omitempty"`
	EventAt     int64  `json:"event_at,omitempty"`
}

type RestoreSourceInfo struct {
	ID                       string `json:"id"`
	Region                   string `json:"region,omitempty"`
	CloudProvider            string `json:"cloud_provider,omitempty"`
	ServicePlan              string `json:"service_plan,omitempty"`
	ExpectedRestoreDataBytes int64  `json:"expected_restore_data_bytes,omitempty"`
}

// RestoreTargetInfo contains target instance attributes useful when a restore
// is stuck in provisioning or has completed but produced an unhealthy target.
type RestoreTargetInfo struct {
	ID              string `json:"id"`
	Name            string `json:"name"`
	State           string `json:"state"`
	Region          string `json:"region"`
	RegionDisplay   string `json:"region_display,omitempty"`
	CloudProvider   string `json:"cloud_provider"`
	ServicePlan     string `json:"service_plan"`
	TiDBVersion     string `json:"tidb_version,omitempty"`
	Creator         string `json:"creator,omitempty"`
	ProjectID       string `json:"project_id,omitempty"`
	OrganizationID  string `json:"organization_id,omitempty"`
	SharedPoolID    string `json:"shared_pool_id,omitempty"`
	DataPlaneID     string `json:"dataplane_id,omitempty"`
	CreateStartedAt int64  `json:"create_started_at,omitempty"`
	CreateReadyAt   int64  `json:"create_ready_at,omitempty"`
	UpdatedAt       int64  `json:"updated_at,omitempty"`
}

// RestoreProgress mirrors `RestoreProgress` in FE types.
type RestoreProgress struct {
	ID              string `json:"id"`
	ProgressPercent int32  `json:"progress_percent"`
	// Stage is the human-readable current step name or overall status string.
	// JSON field is "status" to match the FE RestoreProgress.status field.
	Stage                      string                `json:"status"`
	Steps                      []RestoreStepProgress `json:"steps,omitempty"`
	RateBytesPerSec            int64                 `json:"rate_bytes_per_sec"`
	EtaSeconds                 int64                 `json:"eta_seconds,omitempty"`
	UpdatedAt                  int64                 `json:"updated_at"`
	RestoreBytes               int64                 `json:"restore_bytes,omitempty"`
	Error                      *RestoreErrorDetail   `json:"error,omitempty"`
	Message                    string                `json:"message,omitempty"`
	RestoreStartedAt           int64                 `json:"restore_started_at,omitempty"`
	RestoreFinishedAt          int64                 `json:"restore_finished_at,omitempty"`
	DurationSeconds            int64                 `json:"duration_seconds,omitempty"`
	DataRestoreStep            string                `json:"data_restore_step,omitempty"`
	DataRestoreDurationSeconds int64                 `json:"data_restore_duration_seconds,omitempty"`
	Target                     *RestoreTargetInfo    `json:"target,omitempty"`
	SourceClusterID            string                `json:"source_cluster_id,omitempty"`
	Source                     *RestoreSourceInfo    `json:"source,omitempty"`
	Backup                     *RestoreBackupInfo    `json:"backup,omitempty"`
	Audit                      *RestoreAuditInfo     `json:"audit,omitempty"`
	SourceMetadataAvailable    bool                  `json:"source_metadata_available"`
	SourceMetadataNote         string                `json:"source_metadata_note,omitempty"`
	RuntimeDiagnostic          string                `json:"runtime_diagnostic,omitempty"`
	// NotFound is true when no active restore record exists for the cluster.
	// The HTTP status is still 200; callers should treat this as an empty state.
	NotFound bool `json:"not_found,omitempty"`
}

// ToGetRestoreProgressResp converts a proto RestoreProgressResp to the
// FE-facing RestoreProgress. restoreID is the URL path param echoed
// in the ID field; the RPC is cluster-scoped and does not accept a
// specific restore ID.
//
// Note: dataflow `Progress` is already in the [0, 100] percent range
// (the upstream sets `resp.Progress = 100` for SUCCESS), so we forward
// it directly without scaling.
func ToGetRestoreProgressResp(restoreID uint64, pb *dataflow.RestoreProgressResp) *RestoreProgress {
	out := &RestoreProgress{ID: strconv.FormatUint(restoreID, 10)}
	if pb == nil {
		out.NotFound = true
		return out
	}
	out.ProgressPercent = int32(pb.Progress)
	out.Stage = string(restoreStatus(pb.RestoreStatus))
	if len(pb.Steps) > 0 {
		steps := make([]RestoreStepProgress, 0, len(pb.Steps))
		for _, s := range pb.Steps {
			if s == nil {
				continue
			}
			steps = append(steps, RestoreStepProgress{
				StepName: s.StepName,
				Progress: s.Progress,
			})
		}
		out.Steps = steps
		// Use the first non-empty step name as a human-readable stage label.
		if len(steps) > 0 && steps[0].StepName != "" {
			out.Stage = steps[0].StepName
		}
	}
	out.RestoreBytes = int64(pb.RestoreBytes)
	if pb.RestoreError != nil {
		retryable := pb.RestoreError.Retryable
		out.Error = &RestoreErrorDetail{
			Message:   pb.RestoreError.Message,
			Retryable: &retryable,
		}
	}
	return out
}

func ToGetPremiumRestoreProgressResp(restoreID string, pb *nextgenglobal.RestoreStatusResponse) *RestoreProgress {
	out := &RestoreProgress{ID: restoreID}
	if pb == nil {
		out.NotFound = true
		return out
	}
	out.ProgressPercent = pb.GetProgress()
	out.Stage = string(premiumRestoreStatus(pb.GetState()))
	if premiumRestoreStatus(pb.GetState()) == RestoreStatusFailed {
		out.Error = &RestoreErrorDetail{Message: pb.GetMessage()}
	}
	return out
}

// ToGetPremiumRestoreDiagnosticsResp builds the operator-facing Premium
// restore view from ng-global state/source metadata and target cluster
// metadata. Runtime diagnostic availability is reported separately because
// ng-global does not currently expose the region restore ID.
func ToGetPremiumRestoreDiagnosticsResp(restoreID string, diagnostics *tidbtoolsapi.PremiumRestore) *RestoreProgress {
	out := &RestoreProgress{ID: restoreID}
	if diagnostics == nil || diagnostics.Status == nil {
		out.NotFound = true
		return out
	}

	status := diagnostics.Status
	out.ProgressPercent = status.GetProgress()
	out.Stage = string(premiumRestoreStatus(status.GetState()))
	out.Message = status.GetMessage()
	out.RuntimeDiagnostic = diagnostics.RuntimeError
	if premiumRestoreStatus(status.GetState()) == RestoreStatusFailed {
		out.Error = &RestoreErrorDetail{Message: status.GetMessage()}
	}

	if target := diagnostics.Target; target != nil {
		ng := target.GetNextGenCluster()
		out.Target = &RestoreTargetInfo{
			ID:             strconv.FormatUint(target.GetClusterId(), 10),
			Name:           target.GetClusterName(),
			State:          ng.GetState().String(),
			Region:         target.GetRegion(),
			RegionDisplay:  ng.GetRegionDisplayName(),
			CloudProvider:  target.GetCloudProvider(),
			ServicePlan:    ng.GetServicePlan().String(),
			TiDBVersion:    ng.GetVersion(),
			Creator:        target.GetCreatedBy(),
			ProjectID:      strconv.FormatUint(target.GetProjectId(), 10),
			OrganizationID: strconv.FormatUint(target.GetOrganizationId(), 10),
			SharedPoolID:   ng.GetSharedPoolId(),
			DataPlaneID:    ng.GetDataplaneId(),
		}
		if target.GetCreateTime() != nil {
			out.Target.CreateStartedAt = target.GetCreateTime().AsTime().Unix()
			out.RestoreStartedAt = out.Target.CreateStartedAt
		}
		if ng.GetCreateFinishedTime() != nil {
			out.Target.CreateReadyAt = ng.GetCreateFinishedTime().AsTime().Unix()
			if premiumRestoreStatus(status.GetState()) == RestoreStatusSucceeded {
				out.RestoreFinishedAt = out.Target.CreateReadyAt
			}
		}
		if target.GetUpdateTime() != nil {
			out.Target.UpdatedAt = target.GetUpdateTime().AsTime().Unix()
			out.UpdatedAt = out.Target.UpdatedAt
		}
		if ng.GetSourceClusterId() != 0 {
			out.SourceClusterID = strconv.FormatUint(ng.GetSourceClusterId(), 10)
		}
	}

	if source := diagnostics.Source; source != nil {
		if source.ClusterID != 0 {
			out.SourceClusterID = strconv.FormatUint(source.ClusterID, 10)
			out.Source = &RestoreSourceInfo{
				ID:                       out.SourceClusterID,
				Region:                   source.Region,
				CloudProvider:            source.CloudProvider,
				ServicePlan:              source.ServicePlan,
				ExpectedRestoreDataBytes: source.ExpectedRestoreDataBytes,
			}
		}
		out.Audit = &RestoreAuditInfo{
			RestoreMode: source.RestoreMode,
		}
		if source.PointInTime != nil {
			out.Audit.PointInTime = source.PointInTime.Unix()
		}
		if source.EventAt != nil {
			out.Audit.EventAt = source.EventAt.Unix()
			out.RestoreStartedAt = out.Audit.EventAt
		}
		if source.UpdatedAt != nil {
			out.UpdatedAt = source.UpdatedAt.Unix()
			if premiumRestoreStatus(status.GetState()) == RestoreStatusSucceeded {
				out.RestoreFinishedAt = out.UpdatedAt
			}
		}
		if source.BackupID != "" {
			out.Backup = &RestoreBackupInfo{ID: source.BackupID}
		}
	}
	if backup := diagnostics.Backup; backup != nil {
		if out.Backup == nil {
			out.Backup = &RestoreBackupInfo{ID: backup.GetId()}
		}
		out.Backup.Name = backup.GetDisplayName()
		out.Backup.TSO = backup.GetBackupTs()
		out.Backup.Type = backup.GetType().String()
		out.Backup.Status = backup.GetState().String()
		out.Backup.SizeBytes = backup.GetSize()
		out.Backup.Region = backup.GetRegionId()
		if tso, err := strconv.ParseUint(backup.GetBackupTs(), 10, 64); err == nil {
			out.Backup.CreatedAt = int64(tso>>18) / 1000
		}
		// NextGen exposes the backup resource creation timestamp but no backup
		// completion timestamp. Keep FinishedAt unset rather than fabricating it.
	}
	if out.RestoreStartedAt > 0 && out.RestoreFinishedAt >= out.RestoreStartedAt {
		out.DurationSeconds = out.RestoreFinishedAt - out.RestoreStartedAt
	} else if out.DataRestoreDurationSeconds > 0 {
		out.DurationSeconds = out.DataRestoreDurationSeconds
	}

	out.SourceMetadataAvailable = out.SourceClusterID != "" || out.Backup != nil
	if !out.SourceMetadataAvailable {
		out.SourceMetadataNote = diagnostics.SourceError
	} else if diagnostics.SourceError != "" {
		out.SourceMetadataNote = diagnostics.SourceError
	}
	return out
}

// RestoreDetail is the response body for `GET .../restores/{restore_id}`.
type RestoreDetail struct {
	ID        string              `json:"id"`
	CreatedAt int64               `json:"created_at"`
	BackupID  string              `json:"backup_id"`
	ClusterID string              `json:"cluster_id"`
	Status    RestoreStatus       `json:"status" enums:"pending,running,succeeded,failed,terminated,canceled,unknown"`
	Cluster   *RestoreClusterInfo `json:"cluster,omitempty"`
}

// RestoreClusterInfo holds minimal cluster info embedded in RestoreDetail.
type RestoreClusterInfo struct {
	ID     string `json:"id"`
	Name   string `json:"name"`
	Status string `json:"status"`
}

// ToGetRestoreResp converts a proto GetRestoreResp to the FE-facing RestoreDetail.
func ToGetRestoreResp(pb *dataflow.GetRestoreResp) *RestoreDetail {
	if pb == nil {
		return &RestoreDetail{}
	}
	out := &RestoreDetail{
		ID:        strconv.FormatUint(pb.Id, 10),
		BackupID:  strconv.FormatUint(pb.BackupId, 10),
		ClusterID: strconv.FormatUint(pb.ClusterId, 10),
	}
	if pb.CreateTimestamp != nil {
		out.CreatedAt = pb.CreateTimestamp.AsTime().Unix()
	}
	switch pb.Status {
	case dataflow.GetRestoreResp_PENDING:
		out.Status = RestoreStatusPending
	case dataflow.GetRestoreResp_RUNNING:
		out.Status = RestoreStatusRunning
	case dataflow.GetRestoreResp_FAILED:
		out.Status = RestoreStatusFailed
	case dataflow.GetRestoreResp_SUCCESS:
		out.Status = RestoreStatusSucceeded
	case dataflow.GetRestoreResp_TERMINATED:
		out.Status = RestoreStatusTerminated
	default:
		out.Status = RestoreStatusUnknown
	}
	if pb.Cluster != nil {
		out.Cluster = &RestoreClusterInfo{
			ID:     strconv.FormatUint(pb.Cluster.Id, 10),
			Name:   pb.Cluster.Name,
			Status: pb.Cluster.Status,
		}
	}
	return out
}

func ToGetPremiumRestoreResp(clusterID, restoreID string, pb *nextgenglobal.RestoreStatusResponse) *RestoreDetail {
	if pb == nil {
		return nil
	}
	return &RestoreDetail{
		ID:        restoreID,
		ClusterID: clusterID,
		Status:    premiumRestoreStatus(pb.GetState()),
	}
}

// ToListRestoresResp converts a proto ListRestoresResp to the FE-facing
// ListRestoresResp. See note on ToGetRestoreProgressResp regarding the
// `Progress` field unit.
func ToListRestoresResp(pb *dataflow.ListRestoresResp) *ListRestoresResp {
	if pb == nil {
		return &ListRestoresResp{Items: []*Restore{}}
	}
	items := make([]*Restore, 0, len(pb.Items))
	for _, it := range pb.Items {
		if it == nil {
			continue
		}
		var startedAt int64
		if it.CreatedAt != nil {
			startedAt = it.CreatedAt.AsTime().Unix()
		}
		var finishedAt int64
		if it.FinishedAt != nil {
			finishedAt = it.FinishedAt.AsTime().Unix()
		}
		var restoredAt int64
		if it.RestoredTs != nil {
			restoredAt = it.RestoredTs.AsTime().Unix()
		}
		items = append(items, &Restore{
			ID:               strconv.FormatUint(it.Id, 10),
			Name:             it.Name,
			Mode:             restoreMode(it.RestoreMode),
			SourceBackupName: it.BackupName,
			TargetClusterID:  strconv.FormatUint(it.ClusterId, 10),
			RestoreRegion:    it.RestoreRegion,
			Status:           restoreStatus(it.Status),
			RestoredTS:       it.RestoredTso,
			RestoredAt:       restoredAt,
			StartedAt:        startedAt,
			FinishedAt:       finishedAt,
			ProgressPercent:  int32(it.Progress),
		})
	}
	return &ListRestoresResp{
		Items: items,
		Total: int64(pb.Total),
	}
}

func restoreMode(m dataflow.RestoreMode) RestoreMode {
	switch m {
	case dataflow.RestoreMode_RESTORE_MODE_PITR:
		return RestoreModePITR
	case dataflow.RestoreMode_RESTORE_MODE_SNAPSHOT, dataflow.RestoreMode_RESTORE_MODE_VOLUME_SNAPSHOT:
		return RestoreModeSnapshot
	default:
		return ""
	}
}

func restoreStatus(s dataflow.RestoreStatus) RestoreStatus {
	switch s {
	case dataflow.RestoreStatus_RESTORE_STATUS_PENDING:
		return RestoreStatusPending
	case dataflow.RestoreStatus_RESTORE_STATUS_RUNNING:
		return RestoreStatusRunning
	case dataflow.RestoreStatus_RESTORE_STATUS_FAILED:
		return RestoreStatusFailed
	case dataflow.RestoreStatus_RESTORE_STATUS_SUCCESS:
		return RestoreStatusSucceeded
	case dataflow.RestoreStatus_RESTORE_STATUS_TERMINATED:
		return RestoreStatusTerminated
	default:
		return RestoreStatusUnknown
	}
}

func premiumRestoreStatus(status nextgenglobal.RestoreState) RestoreStatus {
	switch status {
	case nextgenglobal.RestoreState_RESTORE_STATE_PENDING:
		return RestoreStatusPending
	case nextgenglobal.RestoreState_RESTORE_STATE_RUNNING:
		return RestoreStatusRunning
	case nextgenglobal.RestoreState_RESTORE_STATE_SUCCESS:
		return RestoreStatusSucceeded
	case nextgenglobal.RestoreState_RESTORE_STATE_FAILED:
		return RestoreStatusFailed
	case nextgenglobal.RestoreState_RESTORE_STATE_CANCELLED:
		return RestoreStatusCanceled
	default:
		return RestoreStatusUnknown
	}
}
