package api

import (
	"fmt"
	"time"

	"github.com/sacloud/libsacloud/sacloud"
)

var (
	// allowDiskEditTags ディスクの編集可否判定に用いるタグ
	allowDiskEditTags = []string{
		"os-unix",
		"os-linux",
	}

	// bundleInfoWindowsHostClass ディスクの編集可否判定に用いる、BundleInfoでのWindows判定文字列
	bundleInfoWindowsHostClass = "ms_windows"
)

// DiskAPI ディスクAPI
type DiskAPI struct {
	*baseAPI
}

// NewDiskAPI ディスクAPI作成
func NewDiskAPI(client *Client) *DiskAPI {
	return &DiskAPI{
		&baseAPI{
			client: client,
			// FuncGetResourceURL
			FuncGetResourceURL: func() string {
				return "disk"
			},
		},
	}
}

// SortByConnectionOrder 接続順でのソート
func (api *DiskAPI) SortByConnectionOrder(reverse bool) *DiskAPI {
	api.sortBy("ConnectionOrder", reverse)
	return api
}

// WithServerID サーバーID条件
func (api *DiskAPI) WithServerID(id int64) *DiskAPI {
	api.FilterBy("Server.ID", id)
	return api
}

// Create 新規作成
func (api *DiskAPI) Create(value *sacloud.Disk) (*sacloud.Disk, error) {
	//HACK: さくらのAPI側仕様: 戻り値:Successがbool値へ変換できないため文字列で受ける
	type diskResponse struct {
		*sacloud.Response
		// Success
		Success string `json:",omitempty"`
	}
	res := &diskResponse{}

	rawBody := &sacloud.Request{}
	rawBody.Disk = value
	if len(value.DistantFrom) > 0 {
		rawBody.DistantFrom = value.DistantFrom
		value.DistantFrom = []int64{}
	}

	err := api.create(rawBody, res)
	if err != nil {
		return nil, err
	}
	return res.Disk, nil
}

// CreateWithConfig ディスク作成とディスクの修正、サーバ起動(指定されていれば)を１回のAPI呼び出しで実行
func (api *DiskAPI) CreateWithConfig(value *sacloud.Disk, config *sacloud.DiskEditValue, bootAtAvailable bool) (*sacloud.Disk, error) {
	//HACK: さくらのAPI側仕様: 戻り値:Successがbool値へ変換できないため文字列で受ける("Accepted"などが返る)
	type diskResponse struct {
		*sacloud.Response
		// Success
		Success string `json:",omitempty"`
	}
	res := &diskResponse{}

	type diskRequest struct {
		*sacloud.Request
		Config          *sacloud.DiskEditValue `json:",omitempty"`
		BootAtAvailable bool                   `json:",omitempty"`
	}

	rawBody := &diskRequest{
		Request:         &sacloud.Request{},
		BootAtAvailable: bootAtAvailable,
	}
	rawBody.Disk = value
	rawBody.Config = config

	if len(value.DistantFrom) > 0 {
		rawBody.DistantFrom = value.DistantFrom
		value.DistantFrom = []int64{}
	}

	err := api.create(rawBody, res)
	if err != nil {
		return nil, err
	}
	return res.Disk, nil
}

// NewCondig ディスクの修正用パラメーター作成
func (api *DiskAPI) NewCondig() *sacloud.DiskEditValue {
	return &sacloud.DiskEditValue{}
}

// Config ディスクの修正
func (api *DiskAPI) Config(id int64, disk *sacloud.DiskEditValue) (bool, error) {
	var (
		method = "PUT"
		uri    = fmt.Sprintf("%s/%d/config", api.getResourceURL(), id)
	)

	return api.modify(method, uri, disk)
}

func (api *DiskAPI) install(id int64, body *sacloud.Disk) (bool, error) {
	var (
		method = "PUT"
		uri    = fmt.Sprintf("%s/%d/install", api.getResourceURL(), id)
	)
	//HACK: さくらのAPI側仕様: 戻り値:Successがbool値へ変換できないため文字列で受ける
	type diskResponse struct {
		*sacloud.ResultFlagValue
		// Success
		Success string `json:",omitempty"`
	}
	res := &diskResponse{}
	rawBody := &sacloud.Request{}
	rawBody.Disk = body
	if len(body.DistantFrom) > 0 {
		rawBody.DistantFrom = body.DistantFrom
		body.DistantFrom = []int64{}
	}

	err := api.baseAPI.request(method, uri, rawBody, res)
	if err != nil {
		return false, err
	}
	return res.IsOk, nil
}

// ReinstallFromBlank ブランクディスクから再インストール
func (api *DiskAPI) ReinstallFromBlank(id int64, sizeMB int) (bool, error) {
	var body = &sacloud.Disk{}
	body.SetSizeMB(sizeMB)

	return api.install(id, body)
}

// ReinstallFromArchive アーカイブからの再インストール
func (api *DiskAPI) ReinstallFromArchive(id int64, archiveID int64, distantFrom ...int64) (bool, error) {
	var body = &sacloud.Disk{}
	body.SetSourceArchive(archiveID)
	if len(distantFrom) > 0 {
		body.SetDistantFrom(distantFrom)
	}
	return api.install(id, body)
}

// ReinstallFromDisk ディスクからの再インストール
func (api *DiskAPI) ReinstallFromDisk(id int64, diskID int64, distantFrom ...int64) (bool, error) {
	var body = &sacloud.Disk{}
	body.SetSourceDisk(diskID)
	if len(distantFrom) > 0 {
		body.SetDistantFrom(distantFrom)
	}
	return api.install(id, body)
}

// ToBlank ディスクを空にする
func (api *DiskAPI) ToBlank(diskID int64) (bool, error) {
	var (
		method = "PUT"
		uri    = fmt.Sprintf("%s/%d/to/blank", api.getResourceURL(), diskID)
	)
	return api.modify(method, uri, nil)
}

// ResizePartition パーティションのリサイズ
func (api *DiskAPI) ResizePartition(diskID int64) (bool, error) {
	var (
		method = "PUT"
		uri    = fmt.Sprintf("%s/%d/resize-partition", api.getResourceURL(), diskID)
	)
	return api.modify(method, uri, nil)
}

// DisconnectFromServer サーバーとの接続解除
func (api *DiskAPI) DisconnectFromServer(diskID int64) (bool, error) {
	var (
		method = "DELETE"
		uri    = fmt.Sprintf("%s/%d/to/server", api.getResourceURL(), diskID)
	)
	return api.modify(method, uri, nil)
}

// ConnectToServer サーバーとの接続
func (api *DiskAPI) ConnectToServer(diskID int64, serverID int64) (bool, error) {
	var (
		method = "PUT"
		uri    = fmt.Sprintf("%s/%d/to/server/%d", api.getResourceURL(), diskID, serverID)
	)
	return api.modify(method, uri, nil)
}

// State ディスクの状態を取得し有効な状態か判定
func (api *DiskAPI) State(diskID int64) (bool, error) {
	disk, err := api.Read(diskID)
	if err != nil {
		return false, err
	}
	return disk.IsAvailable(), nil
}

// SleepWhileCopying コピー終了まで待機
func (api *DiskAPI) SleepWhileCopying(id int64, timeout time.Duration) error {
	handler := waitingForAvailableFunc(func() (hasAvailable, error) {
		return api.Read(id)
	}, 0)
	return blockingPoll(handler, timeout)
}

// AsyncSleepWhileCopying コピー終了まで待機(非同期)
func (api *DiskAPI) AsyncSleepWhileCopying(id int64, timeout time.Duration) (chan (interface{}), chan (interface{}), chan (error)) {
	handler := waitingForAvailableFunc(func() (hasAvailable, error) {
		return api.Read(id)
	}, 0)
	return poll(handler, timeout)
}

// Monitor アクティビティーモニター取得
func (api *DiskAPI) Monitor(id int64, body *sacloud.ResourceMonitorRequest) (*sacloud.MonitorValues, error) {
	return api.baseAPI.monitor(id, body)
}

// CanEditDisk ディスクの修正が可能か判定
func (api *DiskAPI) CanEditDisk(id int64) (bool, error) {

	disk, err := api.Read(id)
	if err != nil {
		return false, err
	}

	if disk == nil {
		return false, nil
	}

	// BundleInfoがあれば編集不可
	if disk.BundleInfo != nil && disk.BundleInfo.HostClass == bundleInfoWindowsHostClass {
		// Windows
		return false, nil
	}

	// SophosUTMであれば編集不可
	if disk.HasTag("pkg-sophosutm") || disk.IsSophosUTM() {
		return false, nil
	}
	// OPNsenseであれば編集不可
	if disk.HasTag("distro-opnsense") {
		return false, nil
	}
	// Netwiser VEであれば編集不可
	if disk.HasTag("pkg-netwiserve") {
		return false, nil
	}

	// ソースアーカイブ/ソースディスクともに持っていない場合
	if disk.SourceArchive == nil && disk.SourceDisk == nil {
		//ブランクディスクがソース
		return false, nil
	}

	for _, t := range allowDiskEditTags {
		if disk.HasTag(t) {
			// 対応OSインストール済みディスク
			return true, nil
		}
	}

	// ここまできても判定できないならソースに投げる
	if disk.SourceDisk != nil && disk.SourceDisk.Availability != "discontinued" {
		return api.client.Disk.CanEditDisk(disk.SourceDisk.ID)
	}
	if disk.SourceArchive != nil && disk.SourceArchive.Availability != "discontinued" {
		return api.client.Archive.CanEditDisk(disk.SourceArchive.ID)
	}

	return false, nil

}

// GetPublicArchiveIDFromAncestors 祖先の中からパブリックアーカイブのIDを検索
func (api *DiskAPI) GetPublicArchiveIDFromAncestors(id int64) (int64, bool) {

	emptyID := int64(0)

	disk, err := api.Read(id)
	if err != nil {
		return emptyID, false
	}

	if disk == nil {
		return emptyID, false
	}

	// BundleInfoがあれば編集不可
	if disk.BundleInfo != nil && disk.BundleInfo.HostClass == bundleInfoWindowsHostClass {
		// Windows
		return emptyID, false
	}

	// SophosUTMであれば編集不可
	if disk.HasTag("pkg-sophosutm") || disk.IsSophosUTM() {
		return emptyID, false
	}
	// OPNsenseであれば編集不可
	if disk.HasTag("distro-opnsense") {
		return emptyID, false
	}
	// Netwiser VEであれば編集不可
	if disk.HasTag("pkg-netwiserve") {
		return emptyID, false
	}

	for _, t := range allowDiskEditTags {
		if disk.HasTag(t) {
			// 対応OSインストール済みディスク
			return disk.ID, true
		}
	}

	// ここまできても判定できないならソースに投げる
	if disk.SourceDisk != nil && disk.SourceDisk.Availability != "discontinued" {
		return api.client.Disk.GetPublicArchiveIDFromAncestors(disk.SourceDisk.ID)
	}
	if disk.SourceArchive != nil && disk.SourceArchive.Availability != "discontinued" {
		return api.client.Archive.GetPublicArchiveIDFromAncestors(disk.SourceArchive.ID)
	}
	return emptyID, false

}
