package ffs

import (
	"os"
	"syscall"
	"time"

	"github.com/pingcap/log"
	"github.com/tidbcloud/ffs-csi-driver/pkg/state"
	"k8s.io/utils/mount"
)

type Mounter interface {
	Mount() error
}

func newVolume(volume, target string, volContext map[string]string) state.Volume {
	return state.Volume{
		Name: volume,
		Path: target,
		Args: newArgs(volume, target, volContext),
	}
}
func (d *Driver) newMounter(v state.Volume) (Mounter, error) {
	return newFFSMounter(v.Name, v.Path, v.Args)
}

func (d *Driver) publishVolume(name, target string) error {
	v, err := d.state.GetVolumeByName(name)
	if err != nil {
		return err
	}
	if !v.Published.Has(target) {
		v.Published.Add(target)
		d.state.UpdateVolume(v)
	}
	return nil
}

func (d *Driver) unpublishVolume(name, target string) error {
	v, err := d.state.GetVolumeByName(name)
	if err != nil {
		return err
	}
	if v.Published.Has(target) {
		v.Published.Remove(target)
		d.state.UpdateVolume(v)
	}
	return nil
}

func (d *Driver) getVolume(name string) (state.Volume, error) {
	return d.state.GetVolumeByName(name)
}

func (d *Driver) stageVolume(v state.Volume) {
	d.state.UpdateVolume(v)
}

func (d *Driver) unstageVolume(name string) {
	d.state.DeleteVolume(name)
}

func (d *Driver) checkPublishedVolumes(volume state.Volume) {
	for _, targetPath := range volume.Published {
		notMnt, err := mount.IsNotMountPoint(mount.New(""), targetPath)
		if err != nil {
			if IsCorruptedMnt(err) {
				notMnt = true
			} else {
				log.S().Errorf("check target path: %v", err)
				continue
			}
		}
		if !notMnt {
			var statfs syscall.Statfs_t
			err := syscall.Statfs(targetPath, &statfs)
			if err == nil {
				log.S().Debugf("volume %s already mounted to %s", volume.Name, targetPath)
				continue
			} else if !IsCorruptedMnt(err) {
				log.S().Errorf("failed to get metrics: volume %s to %s err %s", volume.Name, targetPath, err.Error())
				continue
			}
		}
		options := []string{"bind"}
		mounter := mount.New("")
		if err := mounter.Mount(volume.Path, targetPath, "", options); err != nil {
			log.S().Errorf("failed to mount %s at %s: %s", volume.Path, targetPath, err.Error())
			continue
		}
		log.S().Infof("restore volume %s successfully published at %s", volume.Name, targetPath)
	}
}

func (d *Driver) check(initial bool) {
	d.mutex.Lock()
	defer d.mutex.Unlock()
	for _, volume := range d.state.GetVolumes() {
		if volume.Name == "" || volume.Path == "" {
			log.S().Warnf("empty volume %v", volume)
			continue
		}
		if initial && (len(volume.Published) > 0 && !ExistMountPoint(volume.Published) || !ExistPod(volume.Published)) {
			log.S().Warnf("volume %s published %v does not exist mount point or pod", volume.Name, volume.Published)
			if err := fuseUnmount(volume.Path); err != nil {
				log.S().Errorf("unmount deleted volume %s path %s err %s", volume.Name, volume.Path, err.Error())
				continue
			}
			err := os.Remove(volume.Path)
			if err != nil && !os.IsNotExist(err) {
				log.S().Errorf("remove deleted volume %s path %s err %s", volume.Name, volume.Path, err.Error())
				continue
			}
			d.state.DeleteVolume(volume.Name)
			log.S().Infof("remove deleted volume %s path %s successfully", volume.Name, volume.Path)
			continue
		}

		notMnt, err := checkMount(volume.Path)
		if err != nil {
			if !IsCorruptedMnt(err) {
				log.S().Errorf("check mount volume %s to %s err %s", volume.Name, volume.Path, err.Error())
				continue
			}
			if err = mount.New("").Unmount(volume.Path); err != nil {
				log.S().Errorf("unmount corrupted volume %s to %s err %s", volume.Name, volume.Path, err.Error())
				continue
			}
			notMnt = true
		}
		if !notMnt {
			var statfs syscall.Statfs_t
			err := syscall.Statfs(volume.Path, &statfs)
			if err == nil {
				log.S().Debugf("is mnt volume %s successfully mounted to %s", volume.Name, volume.Path)
				d.checkPublishedVolumes(volume)
				continue
			} else if !IsCorruptedMnt(err) {
				log.S().Errorf("failed to get metrics: volume %s to %s err %s", volume.Name, volume.Path, err.Error())
				continue
			}
			if err = mount.New("").Unmount(volume.Path); err != nil {
				log.S().Errorf("unmount corrupted volume %s to %s err %s", volume.Name, volume.Path, err.Error())
				continue
			}
		}
		killProcess(volume.Path)
		mounter, err := newFFSMounter(volume.Name, volume.Path, volume.Args)
		if err != nil {
			log.S().Errorf("restore new ffs mounter %s to %s err %s", volume.Name, volume.Path, err.Error())
			continue
		}
		if err := mounter.Mount(); err != nil {
			log.S().Errorf("restore %s to %s err %s", volume.Name, volume.Path, err.Error())
			continue
		}
		log.S().Infof("restore volume %s successfully staged to %s", volume.Name, volume.Path)
		d.checkPublishedVolumes(volume)
	}
}

func (d *Driver) unmountAll() {
	d.mutex.Lock()
	defer d.mutex.Unlock()
	for _, volume := range d.state.GetVolumes() {
		if err := fuseUnmount(volume.Path); err != nil {
			log.S().Errorf("unmount volume %s path %s err %s", volume.Name, volume.Path, err.Error())
		}
		err := os.Remove(volume.Path)
		if err != nil && !os.IsNotExist(err) {
			log.S().Errorf("remove volume %s path %s err %s", volume.Name, volume.Path, err.Error())
			continue
		}
	}
}

func (d *Driver) run() {
	defer d.wg.Done()
	ticker := time.NewTicker(time.Second * 10)
	for {
		select {
		case <-ticker.C:
			d.check(false)
		case <-d.ctx.Done():
			ticker.Stop()
			d.unmountAll()
			return
		}
	}
}
