// Copyright 2022-2025 The sacloud/iaas-api-go Authors
//
// 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 iaas

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/sacloud/iaas-api-go/accessor"
	"github.com/sacloud/iaas-api-go/defaults"
	"github.com/sacloud/iaas-api-go/types"
	"github.com/sacloud/packages-go/wait"
)

// UnexpectedAvailabilityError 予期しないAvailabilityとなった場合のerror
type UnexpectedAvailabilityError struct {
	// Err エラー詳細
	Err error
}

// Error errorインターフェース実装
func (e *UnexpectedAvailabilityError) Error() string {
	return fmt.Sprintf("resource returns unexpected availability value: %s", e.Err.Error())
}

// UnexpectedInstanceStatusError 予期しないInstanceStatusとなった場合のerror
type UnexpectedInstanceStatusError struct {
	// Err エラー詳細
	Err error
}

// Error errorインターフェース実装
func (e *UnexpectedInstanceStatusError) Error() string {
	return fmt.Sprintf("resource returns unexpected instance status value: %s", e.Err.Error())
}

var _ wait.StateWaiter = (*StatePollingWaiter)(nil) // StatePollingWaiterでIaaS固有の事情を考慮したwait.StateWaiterを実装する

// StatePollingWaiter ポーリングによりリソースの状態が変わるまで待機する
type StatePollingWaiter struct {
	// ReadFunc 対象リソースの状態を取得するためのfunc
	ReadFunc wait.StateReadFunc

	// StateCheckFunc ReadFuncで得たリソースの情報を元に待ちを継続するかの判定を行うためのfunc
	StateCheckFunc wait.StateCheckFunc

	// Timeout タイムアウト
	Timeout time.Duration // タイムアウト

	// Interval ポーリング間隔
	Interval time.Duration

	// NotFoundRetry Readで404が返ってきた場合のリトライ回数
	//
	// アプライアンスなどの一部のリソースでは作成~起動完了までの間に404を返すことがある。
	// これに対応するためこのフィールドにて404発生の許容回数を指定可能にする。
	NotFoundRetry int

	// TargetAvailability 対象リソースのAvailabilityがこの状態になった場合になるまで待つ
	//
	// この値を指定する場合、ReadFuncにてAvailabilityHolderを返す必要がある。
	// AvailabilityがTargetAvailabilityとPendingAvailabilityで指定されていない状態になった場合はUnexpectedAvailabilityErrorを返す
	//
	// TargetAvailability(Pending)とTargetInstanceState(Pending)の両方が指定された場合は両方を満たすまで待つ
	// StateCheckFuncとの併用は不可。併用した場合はpanicする。
	TargetAvailability []types.EAvailability

	// PendingAvailability 対象リソースのAvailabilityがこの状態になった場合は待ちを継続する。
	//
	// 詳細はTargetAvailabilityのコメントを参照
	PendingAvailability []types.EAvailability

	// TargetInstanceStatus 対象リソースのInstanceStatusがこの状態になった場合になるまで待つ
	//
	// この値を指定する場合、ReadFuncにてInstanceStatusHolderを返す必要がある。
	// InstanceStatusがTargetInstanceStatusとPendingInstanceStatusで指定されていない状態になった場合はUnexpectedInstanceStatusErrorを返す
	//
	// TargetAvailabilityとTargetInstanceStateの両方が指定された場合は両方を満たすまで待つ
	//
	// StateCheckFuncとの併用は不可。併用した場合はpanicする。
	TargetInstanceStatus []types.EServerInstanceStatus

	// PendingInstanceStatus 対象リソースのInstanceStatusがこの状態になった場合は待ちを継続する。
	//
	// 詳細はTargetInstanceStatusのコメントを参照
	PendingInstanceStatus []types.EServerInstanceStatus

	// RaiseErrorWithUnknownState State(AvailabilityとInstanceStatus)が予期しない値だった場合にエラーとするか
	RaiseErrorWithUnknownState bool
}

// WaitForState リソースが指定の状態になるまで待つ
func (w *StatePollingWaiter) WaitForState(ctx context.Context) (interface{}, error) {
	c, p, e := w.WaitForStateAsync(ctx)
	for {
		select {
		case <-ctx.Done():
			return nil, ctx.Err()
		case lastState := <-c:
			return lastState, nil
		case <-p:
			// noop
		case err := <-e:
			return nil, err
		}
	}
}

// WaitForStateAsync リソースが指定の状態になるまで待つ
func (w *StatePollingWaiter) WaitForStateAsync(ctx context.Context) (<-chan interface{}, <-chan interface{}, <-chan error) {
	w.validateFields()
	if w.Timeout == time.Duration(0) {
		w.Timeout = defaults.DefaultStatePollingTimeout
	}
	if w.Interval == time.Duration(0) {
		w.Interval = defaults.DefaultStatePollingInterval
	}

	waiter := wait.PollingWaiter{
		ReadFunc:       w.readFunc(),
		StateCheckFunc: w.stateCheckFunc,
		Timeout:        w.Timeout,
		Interval:       w.Interval,
	}

	return waiter.WaitForStateAsync(ctx)
}

func (w *StatePollingWaiter) readFunc() func() (interface{}, error) {
	notFoundCounter := w.NotFoundRetry
	return func() (interface{}, error) {
		read, err := w.ReadFunc()
		if err != nil {
			if IsNotFoundError(err) {
				notFoundCounter--
				if notFoundCounter >= 0 {
					return nil, nil
				}
			}
			return nil, err
		}
		return read, err
	}
}

func (w *StatePollingWaiter) validateFields() {
	if w.ReadFunc == nil {
		panic(errors.New("StatePollingWaiter has invalid setting: ReadFunc is required"))
	}

	if w.StateCheckFunc != nil && (len(w.TargetAvailability) > 0 || len(w.TargetInstanceStatus) > 0) {
		panic(errors.New("StatePollingWaiter has invalid setting: StateCheckFunc and TargetAvailability/TargetInstanceStatus can not use together"))
	}

	if w.StateCheckFunc == nil && len(w.TargetAvailability) == 0 && len(w.TargetInstanceStatus) == 0 {
		panic(errors.New("StatePollingWaiter has invalid setting: TargetAvailability or TargetInstanceState must have least 1 items when StateCheckFunc is not set"))
	}
}

func (w *StatePollingWaiter) stateCheckFunc(state interface{}) (bool, error) {
	if w.StateCheckFunc != nil {
		return w.StateCheckFunc(state)
	}

	availabilityHolder, hasAvailability := state.(accessor.Availability)
	instanceStateHolder, hasInstanceState := state.(accessor.InstanceStatus)

	switch {
	case hasAvailability && hasInstanceState:

		res1, err := w.handleAvailability(availabilityHolder)
		if err != nil {
			return false, err
		}
		res2, err := w.handleInstanceState(instanceStateHolder)
		if err != nil {
			return false, err
		}
		return res1 && res2, nil

	case hasAvailability:
		return w.handleAvailability(availabilityHolder)
	case hasInstanceState:
		return w.handleInstanceState(instanceStateHolder)
	default:
		// どちらのインターフェースも実装していない場合、stateが存在するだけでtrueとする
		return true, nil
	}
}

func (w *StatePollingWaiter) handleAvailability(state accessor.Availability) (bool, error) {
	if len(w.TargetAvailability) == 0 {
		return true, nil
	}
	v := state.GetAvailability()
	switch {
	case w.isInAvailability(v, w.TargetAvailability):
		return true, nil
	case w.isInAvailability(v, w.PendingAvailability):
		return false, nil
	default:
		var err error
		if w.RaiseErrorWithUnknownState {
			err = fmt.Errorf("got unexpected value of Availability: got %q", v)
		}
		return false, err
	}
}

func (w *StatePollingWaiter) handleInstanceState(state accessor.InstanceStatus) (bool, error) {
	if len(w.TargetInstanceStatus) == 0 {
		return true, nil
	}
	v := state.GetInstanceStatus()
	switch {
	case w.isInInstanceStatus(v, w.TargetInstanceStatus):
		return true, nil
	case w.isInInstanceStatus(v, w.PendingInstanceStatus):
		return false, nil
	default:
		var err error
		if w.RaiseErrorWithUnknownState {
			err = fmt.Errorf("got unexpected value of InstanceState: got %q", v)
		}
		return false, err
	}
}

func (w *StatePollingWaiter) isInAvailability(v types.EAvailability, conditions []types.EAvailability) bool {
	for _, cond := range conditions {
		if v == cond {
			return true
		}
	}
	return false
}

func (w *StatePollingWaiter) isInInstanceStatus(v types.EServerInstanceStatus, conditions []types.EServerInstanceStatus) bool {
	for _, cond := range conditions {
		if v == cond {
			return true
		}
	}
	return false
}
