package iovfs

import (
	"fmt"
	"io/fs"
	"strings"
	"time"

	"github.com/microsoft/typescript-go/internal/tspath"
	"github.com/microsoft/typescript-go/internal/vfs"
	"github.com/microsoft/typescript-go/internal/vfs/internal"
)

type RealpathFS interface {
	fs.FS
	Realpath(path string) (string, error)
}

type WritableFS interface {
	fs.FS
	WriteFile(path string, data string, perm fs.FileMode) error
	AppendFile(path string, data string, perm fs.FileMode) error
	MkdirAll(path string, perm fs.FileMode) error
	// Removes `path` and all its contents. Will return the first error it encounters.
	Remove(path string) error
	Chtimes(path string, aTime time.Time, mTime time.Time) error
}

type FsWithSys interface {
	vfs.FS
	FSys() fs.FS
}

// From creates a new FS from an [fs.FS].
//
// For paths like `c:/foo/bar`, fsys will be used as though it's rooted at `/` and the path is `/c:/foo/bar`.
//
// If the provided [fs.FS] implements [RealpathFS], it will be used to implement the Realpath method.
// If the provided [fs.FS] implements [WritableFS], it will be used to implement the WriteFile method.
//
// From does not actually handle case-insensitivity; ensure the passed in [fs.FS]
// respects case-insensitive file names if needed. Consider using [vfstest.FromMap] for testing.
func From(fsys fs.FS, useCaseSensitiveFileNames bool) FsWithSys {
	var realpath func(path string) (string, error)
	if fsys, ok := fsys.(RealpathFS); ok {
		realpath = func(path string) (string, error) {
			rest, hadSlash := strings.CutPrefix(path, "/")
			rp, err := fsys.Realpath(rest)
			if err != nil {
				return "", err
			}
			if hadSlash {
				return "/" + rp, nil
			}
			return rp, nil
		}
	} else {
		realpath = func(path string) (string, error) {
			return path, nil
		}
	}

	var writeFile func(path string, content string) error
	var appendFile func(path string, content string) error
	var mkdirAll func(path string) error
	var remove func(path string) error
	var chtimes func(path string, aTime time.Time, mTime time.Time) error
	if fsys, ok := fsys.(WritableFS); ok {
		writeFile = func(path string, content string) error {
			rest, _ := strings.CutPrefix(path, "/")
			return fsys.WriteFile(rest, content, 0o666)
		}
		appendFile = func(path string, content string) error {
			rest, _ := strings.CutPrefix(path, "/")
			return fsys.AppendFile(rest, content, 0o666)
		}
		mkdirAll = func(path string) error {
			rest, _ := strings.CutPrefix(path, "/")
			return fsys.MkdirAll(rest, 0o777)
		}
		remove = func(path string) error {
			rest, _ := strings.CutPrefix(path, "/")
			return fsys.Remove(rest)
		}
		chtimes = func(path string, aTime time.Time, mTime time.Time) error {
			rest, _ := strings.CutPrefix(path, "/")
			return fsys.Chtimes(rest, aTime, mTime)
		}
	} else {
		writeFile = func(string, string) error {
			panic("writeFile not supported")
		}
		appendFile = func(string, string) error {
			panic("appendFile not supported")
		}
		mkdirAll = func(string) error {
			panic("mkdirAll not supported")
		}
		remove = func(string) error {
			panic("remove not supported")
		}
		chtimes = func(string, time.Time, time.Time) error {
			panic("chtimes not supported")
		}
	}

	return &ioFS{
		common: internal.Common{
			RootFor: func(root string) fs.FS {
				if root == "/" {
					return fsys
				}

				p := tspath.RemoveTrailingDirectorySeparator(root)
				sub, err := fs.Sub(fsys, p)
				if err != nil {
					if tspath.IsUrl(root) {
						return nil
					}
					panic(fmt.Sprintf("vfs: failed to create sub file system for %q: %v", p, err))
				}
				return sub
			},
		},
		useCaseSensitiveFileNames: useCaseSensitiveFileNames,
		realpath:                  realpath,
		writeFile:                 writeFile,
		appendFile:                appendFile,
		mkdirAll:                  mkdirAll,
		remove:                    remove,
		chtimes:                   chtimes,
		fsys:                      fsys,
	}
}

type ioFS struct {
	common internal.Common

	useCaseSensitiveFileNames bool
	realpath                  func(path string) (string, error)
	writeFile                 func(path string, content string) error
	appendFile                func(path string, content string) error
	mkdirAll                  func(path string) error
	remove                    func(path string) error
	chtimes                   func(path string, aTime time.Time, mTime time.Time) error
	fsys                      fs.FS
}

var _ FsWithSys = (*ioFS)(nil)

func (vfs *ioFS) UseCaseSensitiveFileNames() bool {
	return vfs.useCaseSensitiveFileNames
}

func (vfs *ioFS) DirectoryExists(path string) bool {
	return vfs.common.DirectoryExists(path)
}

func (vfs *ioFS) FileExists(path string) bool {
	return vfs.common.FileExists(path)
}

func (vfs *ioFS) GetAccessibleEntries(path string) vfs.Entries {
	return vfs.common.GetAccessibleEntries(path)
}

func (vfs *ioFS) Stat(path string) vfs.FileInfo {
	_ = internal.RootLength(path) // Assert path is rooted
	return vfs.common.Stat(path)
}

func (vfs *ioFS) ReadFile(path string) (contents string, ok bool) {
	return vfs.common.ReadFile(path)
}

func (vfs *ioFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error {
	return vfs.common.WalkDir(root, walkFn)
}

func (vfs *ioFS) Remove(path string) error {
	_ = internal.RootLength(path) // Assert path is rooted
	return vfs.remove(path)
}

func (vfs *ioFS) Chtimes(path string, aTime time.Time, mTime time.Time) error {
	_ = internal.RootLength(path) // Assert path is rooted
	return vfs.chtimes(path, aTime, mTime)
}

func (vfs *ioFS) Realpath(path string) string {
	root, rest := internal.SplitPath(path)
	// splitPath normalizes the path into parts (e.g. "c:/foo/bar" -> "c:/", "foo/bar")
	// Put them back together to call realpath.
	realpath, err := vfs.realpath(root + rest)
	if err != nil {
		return path
	}
	return realpath
}

func (vfs *ioFS) writeFileEnsuringDir(path string, content string, write func(path, content string) error) error {
	_ = internal.RootLength(path) // Assert path is rooted
	if err := write(path, content); err == nil {
		return nil
	}
	if err := vfs.mkdirAll(tspath.GetDirectoryPath(tspath.NormalizePath(path))); err != nil {
		return err
	}
	return write(path, content)
}

func (vfs *ioFS) WriteFile(path string, content string) error {
	return vfs.writeFileEnsuringDir(path, content, vfs.writeFile)
}

func (vfs *ioFS) AppendFile(path string, content string) error {
	return vfs.writeFileEnsuringDir(path, content, vfs.appendFile)
}

func (vfs *ioFS) FSys() fs.FS {
	return vfs.fsys
}
