// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.

package resource

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"gopkg.in/yaml.v3"
	"io"
	"io/fs"
	"net/url"
	"os"
	"os/user"
	"path/filepath"
	"strconv"
	"syscall"
	"time"
	"crypto/sha256"
	"gitea.rosskeen.house/rosskeen.house/machine"
)

type FileType string

const (
	RegularFile         FileType = "regular"
	DirectoryFile       FileType = "directory"
	BlockDeviceFile     FileType = "block"
	CharacterDeviceFile FileType = "char"
	NamedPipeFile       FileType = "pipe"
	SymbolicLinkFile    FileType = "symlink"
	SocketFile          FileType = "socket"
)

var ErrInvalidResourceURI error = errors.New("Invalid resource URI")
var ErrInvalidFileInfo error = errors.New("Invalid FileInfo")
var ErrInvalidFileMode error = errors.New("Invalid Mode")
var ErrInvalidFileOwner error = errors.New("Unknown User")
var ErrInvalidFileGroup error = errors.New("Unknown Group")

func init() {
	ResourceTypes.Register("file", func(u *url.URL) Resource {
		f := NewFile()
		f.Path = filepath.Join(u.Hostname(), u.Path)
		return f
	})
}

// Manage the state of file system objects
type File struct {
	normalizePath bool `json:"-" yaml:"-"`
	Path   string `json:"path" yaml:"path"`
	Owner  string `json:"owner" yaml:"owner"`
	Group  string `json:"group" yaml:"group"`
	Mode   string `json:"mode" yaml:"mode"`

	Atime time.Time `json:"atime,omitempty" yaml:"atime,omitempty"`
	Ctime time.Time `json:"ctime,omitempty" yaml:"ctime,omitempty"`
	Mtime time.Time `json:"mtime,omitempty" yaml:"mtime,omitempty"`

	Content  string   `json:"content,omitempty" yaml:"content,omitempty"`
	Sha256   string   `json:"sha256,omitempty" yaml:"sha256,omitempty"`
	Size     int64    `json:"size,omitempty" yaml:"size,omitempty"`
	Target   string   `json:"target,omitempty" yaml:"target,omitempty"`
	FileType FileType `json:"filetype" yaml:"filetype"`
	State    string   `json:"state,omitempty" yaml:"state,omitempty"`
}

type ResourceFileInfo struct {
	resource *File
}

func NewFile() *File {
	currentUser, _ := user.Current()
	group, _ := user.LookupGroupId(currentUser.Gid)
	f := &File{ normalizePath: false, Owner: currentUser.Username, Group: group.Name, Mode: "0644", FileType: RegularFile}
	slog.Info("NewFile()", "file", f)
	return f
}

func NewNormalizedFile() *File {
	f := NewFile()
	f.normalizePath = true
	return f
}

func (f *File) Clone() Resource {
	return &File {
		normalizePath: f.normalizePath,
		Path: f.Path,
		Owner: f.Owner,
		Group: f.Group,
		Mode: f.Mode,
		Atime: f.Atime,
		Ctime: f.Ctime,
		Mtime: f.Mtime,
		Content: f.Content,
		Sha256: f.Sha256,
		Size: f.Size,
		Target: f.Target,
		FileType: f.FileType,
		State: f.State,
	}
}

func (f *File) StateMachine() machine.Stater {
	return StorageMachine()
}

func (f *File) URI() string {
	return fmt.Sprintf("file://%s", f.Path)
}

func (f *File) SetURI(uri string) error {
	resourceUri, e := url.Parse(uri)
	if e == nil {
		if resourceUri.Scheme == "file" {
			f.Path = filepath.Join(resourceUri.Hostname(), resourceUri.RequestURI())
			if err := f.NormalizePath(); err != nil {
				return err
			}
		} else {
			e = fmt.Errorf("%w: %s is not a file", ErrInvalidResourceURI, uri)
		}
	}
	return e
}

func (f *File) Validate() error {
	return fmt.Errorf("failed")
}

func (f *File) Apply() error {
	switch f.State {
	case "absent":
		removeErr := os.Remove(f.Path)
		if removeErr != nil {
			return removeErr
		}
	case "present":
		{
			uid, uidErr := LookupUID(f.Owner)
			if uidErr != nil {
				return fmt.Errorf("%w: unkwnon user %d", ErrInvalidFileOwner, uid)
			}

			gid, gidErr := LookupGID(f.Group)
			if gidErr != nil {
				return gidErr
			}

			slog.Info("File.Mode", "mode", f.Mode)
			mode, modeErr := strconv.ParseInt(f.Mode, 8, 64)
			if modeErr != nil {
				return fmt.Errorf("%w: invalid mode %d", ErrInvalidFileMode, mode)
			}

			//e := os.Stat(f.path)
			//if os.IsNotExist(e) {
			switch f.FileType {
			case SymbolicLinkFile:
				linkErr := os.Symlink(f.Target, f.Path)
				if linkErr != nil {
					return linkErr
				}
			case DirectoryFile:
				if mkdirErr := os.MkdirAll(f.Path, os.FileMode(mode)); mkdirErr != nil {
					return mkdirErr
				}
			default:
				fallthrough
			case RegularFile:
				createdFile, e := os.Create(f.Path)
				if e != nil {
					return e
				}
				defer createdFile.Close()

				if chmodErr := createdFile.Chmod(os.FileMode(mode)); chmodErr != nil {
					return chmodErr
				}
				_, writeErr := createdFile.Write([]byte(f.Content))
				if writeErr != nil {
					return writeErr
				}
				if !f.Mtime.IsZero() && !f.Atime.IsZero() {
					if chtimesErr := os.Chtimes(f.Path, f.Atime, f.Mtime); chtimesErr != nil {
						return chtimesErr
					}
				}
			}

			if chownErr := os.Chown(f.Path, uid, gid); chownErr != nil {
				return chownErr
			}

		}
	}

	return nil
}

func (f *File) LoadDecl(yamlResourceDeclaration string) (err error) {
	d := NewYAMLStringDecoder(yamlResourceDeclaration)
	err = d.Decode(f)
	if err == nil {
		f.UpdateContentAttributes()
	}
	return
}

func (f *File) ResolveId(ctx context.Context) string {
	if e := f.NormalizePath(); e != nil {
		panic(e)
	}
	return f.Path
}

func (f *File) NormalizePath() error {
	if f.normalizePath {
		filePath, fileAbsErr := filepath.Abs(f.Path)
		if fileAbsErr == nil {
			f.Path = filePath
		}
		return fileAbsErr
	}
	return nil
}

func (f *File) FileInfo() fs.FileInfo {
	return &ResourceFileInfo{ resource: f }
}

func (f *ResourceFileInfo) Name() string {
//	return filepath.Base(f.resource.Path)
	return f.resource.Path
}

func (f *ResourceFileInfo) Size() int64 {
	return f.resource.Size
}

func (f *ResourceFileInfo) Mode() (mode os.FileMode) {
	if fileMode, fileModeErr := strconv.ParseInt(f.resource.Mode, 8, 64); fileModeErr == nil {
		mode |= os.FileMode(fileMode)
	} else {
		panic(fileModeErr)
	}
	mode |= f.resource.FileType.GetMode()
	return
} 

func (f *ResourceFileInfo) ModTime() time.Time {
	return f.resource.Mtime
}

func (f *ResourceFileInfo) IsDir() bool {
	return f.resource.FileType == DirectoryFile
}

func (f *ResourceFileInfo) Sys() any {
	return nil
}

func (f *File) UpdateContentAttributes() {
	f.Size = int64(len(f.Content))
	f.Sha256 = fmt.Sprintf("%x", sha256.Sum256([]byte(f.Content)))
}

func (f *File) UpdateAttributesFromFileInfo(info os.FileInfo) error {
	if info != nil {
		f.Mtime = info.ModTime()
		if stat, ok := info.Sys().(*syscall.Stat_t); ok {
			f.Atime = time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec))
			f.Ctime = time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec))

			userId := strconv.Itoa(int(stat.Uid))
			groupId := strconv.Itoa(int(stat.Gid))
			fileUser, userErr := user.LookupId(userId)
			if userErr != nil { //UnknownUserIdError
				//panic(userErr)
				f.Owner = userId
			} else {
				f.Owner = fileUser.Username
			}
			fileGroup, groupErr := user.LookupGroupId(groupId)
			if groupErr != nil {
				//panic(groupErr)
				f.Group = groupId
			} else {
				f.Group = fileGroup.Name
			}
		}
		f.Size = info.Size()
		f.Mode = fmt.Sprintf("%04o", info.Mode().Perm())
		f.FileType.SetMode(info.Mode())
		return nil
	}
	return ErrInvalidFileInfo
}

func (f *File) ReadStat() error {
	info, e := os.Lstat(f.Path)
	if e != nil {
		f.State = "absent"
		return e
	}

	return f.UpdateAttributesFromFileInfo(info)
}

func (f *File) Read(ctx context.Context) ([]byte, error) {
	if normalizePathErr := f.NormalizePath(); normalizePathErr != nil {
		return nil, normalizePathErr
	}
	
	statErr := f.ReadStat()
	if statErr != nil {
		return nil, statErr
	}

	switch f.FileType {
	case RegularFile:
		file, fileErr := os.Open(f.Path)
		if fileErr != nil {
			panic(fileErr)
		}

		fileContent, ioErr := io.ReadAll(file)
		if ioErr != nil {
			panic(ioErr)
		}
		f.Content = string(fileContent)
		f.Sha256 = fmt.Sprintf("%x", sha256.Sum256(fileContent))
	case SymbolicLinkFile:
		linkTarget, pathErr := os.Readlink(f.Path)
		if pathErr != nil {
			return nil, pathErr
		}
		f.Target = linkTarget
	}
	f.State = "present"
	return yaml.Marshal(f)
}

func (f *File) Type() string { return "file" }

func (f *FileType) UnmarshalYAML(value *yaml.Node) error {
	var s string
	if err := value.Decode(&s); err != nil {
		return err
	}

	switch s {
	case string(RegularFile), string(DirectoryFile), string(BlockDeviceFile), string(CharacterDeviceFile), string(NamedPipeFile), string(SymbolicLinkFile), string(SocketFile):
		*f = FileType(s)
		return nil
	default:
		return errors.New("invalid FileType value")
	}
}

func (f *FileType) SetMode(mode os.FileMode) {
	switch true {
	case mode.IsRegular():
		*f = RegularFile
	case mode.IsDir():
		*f = DirectoryFile
	case mode&os.ModeSymlink != 0:
		*f = SymbolicLinkFile
	case mode&os.ModeNamedPipe != 0:
		*f = NamedPipeFile
	case mode&os.ModeSocket != 0:
		*f = SocketFile
	case mode&os.ModeCharDevice != 0:
		*f = CharacterDeviceFile
	case mode&os.ModeDevice != 0:
		*f = BlockDeviceFile
	}
}

func (f *FileType) GetMode() (mode os.FileMode) {
	switch *f {
	case RegularFile:
	case DirectoryFile:
		mode |= os.ModeDir
	case SymbolicLinkFile:
		mode |= os.ModeSymlink
	case NamedPipeFile:
		mode |= os.ModeNamedPipe
	case SocketFile:
		mode |= os.ModeSocket
	case CharacterDeviceFile:
		mode |= os.ModeCharDevice
	case BlockDeviceFile:
		mode |= os.ModeDevice
	}
	return
}
