package g

import (
	"bufio"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"net/http"
	"os"
	"path/filepath"

	"github.com/enetx/g/internal/filelock"
)

// NewFile returns a new File instance with the given name.
func NewFile[T ~string](name T) *File { return &File{name: String(name)} }

// Lines returns a new iterator instance that can be used to read the file
// line by line.
//
// Example usage:
//
//	// Open a new file with the specified name "text.txt"
//	g.NewFile("text.txt").
//		Lines().                 // Read the file line by line
//		Skip(3).                 // Skip the first 3 lines
//		Exclude(f.Zero).         // Exclude lines that are empty or contain only whitespaces
//		Dedup().                 // Remove consecutive duplicate lines
//		Map(g.String.Upper).     // Convert each line to uppercase
//		ForEach(                 // For each line, print it
//			func(func(s Result[String]) {
//				s.Ok().Print()
//			})
//
//	// Output:
//	// UPPERCASED_LINE4
//	// UPPERCASED_LINE5
//	// UPPERCASED_LINE6
func (f *File) Lines() SeqResult[String] {
	return func(yield func(Result[String]) bool) {
		if f.file == nil {
			if r := f.Open(); r.IsErr() {
				yield(Err[String](r.err))
				return
			}
		}

		defer f.Close()

		scanner := bufio.NewScanner(f.file)
		scanner.Split(bufio.ScanLines)

		for scanner.Scan() {
			if !yield(Ok(String(scanner.Text()))) {
				return
			}
		}

		if err := scanner.Err(); err != nil {
			yield(Err[String](err))
			return
		}
	}
}

// LinesRaw returns a new iterator instance that reads the file line by line,
// yielding each line as a Bytes slice (raw []byte).
//
// This version avoids intermediate string allocations by working directly with byte slices.
// The returned Bytes are copies of the scanner buffer and are safe to retain.
//
// Returns:
//
// - SeqResult[Bytes]: An iterator over raw byte lines from the file.
//
// Example usage:
//
//	g.NewFile("text.txt").
//		LinesRaw().                // Read raw byte lines
//		Filter(func(b g.Bytes) bool {
//			return len(b) > 0
//		}).
//		ForEach(func(line g.Result[g.Bytes]) {
//			line.Ok().Print()
//		})
//
// Output:
// LINE_1
// LINE_2
// ...
//
// Note: Each line is copied before yielding to avoid scanner buffer reuse issues.
func (f *File) LinesRaw() SeqResult[Bytes] {
	return func(yield func(Result[Bytes]) bool) {
		if f.file == nil {
			if r := f.Open(); r.IsErr() {
				yield(Err[Bytes](r.err))
				return
			}
		}

		defer f.Close()

		scanner := bufio.NewScanner(f.file)
		scanner.Split(bufio.ScanLines)

		for scanner.Scan() {
			line := make(Bytes, len(scanner.Bytes()))
			copy(line, scanner.Bytes())
			if !yield(Ok(line)) {
				return
			}
		}

		if err := scanner.Err(); err != nil {
			yield(Err[Bytes](err))
			return
		}
	}
}

// Chunks returns a new iterator instance that can be used to read the file
// in fixed-size chunks of the specified size in bytes.
//
// Parameters:
//
// - size (int): The size of each chunk in bytes.
//
// Example usage:
//
//	// Open a new file with the specified name "text.txt"
//	g.NewFile("text.txt").
//		Chunks(100).              // Read the file in chunks of 100 bytes
//		Map(g.String.Upper).      // Convert each chunk to uppercase
//		ForEach(                  // For each line, print it
//			func(func(s Result[String]) {
//				s.Ok().Print()
//			})
//
//	// Output:
//	// UPPERCASED_CHUNK1
//	// UPPERCASED_CHUNK2
//	// UPPERCASED_CHUNK3
func (f *File) Chunks(size Int) SeqResult[String] {
	return func(yield func(Result[String]) bool) {
		if size.Lte(0) {
			yield(Err[String](errors.New("chunk size must be > 0")))
			return
		}

		if f.file == nil {
			if r := f.Open(); r.IsErr() {
				yield(Err[String](r.err))
				return
			}
		}

		defer f.Close()

		buffer := make([]byte, size)

		for {
			n, err := f.file.Read(buffer)
			if err != nil && err != io.EOF {
				yield(Err[String](err))
				return
			}

			if n == 0 {
				break
			}

			if !yield(Ok(String(buffer[:n]))) {
				return
			}
		}
	}
}

// ChunksRaw returns a new iterator instance that reads the file in fixed-size
// chunks of bytes, yielding each chunk as a Bytes slice.
//
// This method avoids intermediate string allocations and operates directly on byte slices.
// Each chunk is copied from the underlying buffer to make it safe for downstream use.
//
// Parameters:
//
// - size (Int): The size of each chunk in bytes. Must be > 0.
//
// Returns:
//
// - SeqResult[Bytes]: An iterator over raw byte chunks from the file.
//
// Example usage:
//
//	g.NewFile("text.txt").
//		ChunksRaw(128).            // Read raw 128-byte chunks
//		ForEach(func(chunk g.Result[g.Bytes]) {
//			chunk.Ok().Print()
//		})
//
// Output:
// RAW_CHUNK_1
// RAW_CHUNK_2
// ...
//
// Note: Each chunk is copied from the buffer to ensure memory safety.
func (f *File) ChunksRaw(size Int) SeqResult[Bytes] {
	return func(yield func(Result[Bytes]) bool) {
		if size.Lte(0) {
			yield(Err[Bytes](errors.New("chunk size must be > 0")))
			return
		}

		if f.file == nil {
			if r := f.Open(); r.IsErr() {
				yield(Err[Bytes](r.err))
				return
			}
		}

		defer f.Close()

		buf := make([]byte, size)

		for {
			n, err := f.file.Read(buf)
			if err != nil && err != io.EOF {
				yield(Err[Bytes](err))
				return
			}

			if n == 0 {
				break
			}

			chunk := make(Bytes, n)
			copy(chunk, buf[:n])

			if !yield(Ok(chunk)) {
				return
			}
		}
	}
}

// Append appends the given content to the file, with the specified mode (optional).
// If no FileMode is provided, the default FileMode (0644) is used.
// Don't forget to close the file!
func (f *File) Append(content String, mode ...os.FileMode) Result[*File] {
	if f.file == nil {
		if r := f.createAll(); r.IsErr() {
			return r
		}

		fmode := Slice[os.FileMode](mode).Get(0).UnwrapOr(FileDefault)

		if r := f.OpenFile(os.O_APPEND|os.O_CREATE|os.O_WRONLY, fmode); r.IsErr() {
			return r
		}
	}

	if _, err := f.file.WriteString(content.Std()); err != nil {
		return Err[*File](err)
	}

	return Ok(f)
}

// Chmod changes the mode of the file.
func (f *File) Chmod(mode os.FileMode) Result[*File] {
	var err error
	if f.file != nil {
		err = f.file.Chmod(mode)
	} else {
		err = os.Chmod(f.name.Std(), mode)
	}

	if err != nil {
		return Err[*File](err)
	}

	return Ok(f)
}

// Chown changes the owner of the file.
func (f *File) Chown(uid, gid int) Result[*File] {
	var err error
	if f.file != nil {
		err = f.file.Chown(uid, gid)
	} else {
		err = os.Chown(f.name.Std(), uid, gid)
	}

	if err != nil {
		return Err[*File](err)
	}

	return Ok(f)
}

// Seek sets the file offset for the next Read or Write operation. The offset
// is specified by the 'offset' parameter, and the 'whence' parameter determines
// the reference point for the offset.
//
// The 'offset' parameter specifies the new offset in bytes relative to the
// reference point determined by 'whence'. If 'whence' is set to io.SeekStart,
// io.SeekCurrent, or io.SeekEnd, the offset is relative to the start of the file,
// the current offset, or the end of the file, respectively.
//
// If the file is not open, this method will attempt to open it. If the open
// operation fails, an error is returned.
//
// If the Seek operation fails, the file is closed, and an error is returned.
//
// Example:
//
//	file := g.NewFile("example.txt")
//	result := file.Seek(100, io.SeekStart)
//	if result.Err() != nil {
//	    log.Fatal(result.Err())
//	}
//
// Parameters:
//   - offset: The new offset in bytes.
//   - whence: The reference point for the offset (io.SeekStart, io.SeekCurrent, or io.SeekEnd).
//
// Don't forget to close the file!
func (f *File) Seek(offset int64, whence int) Result[*File] {
	if f.file == nil {
		if r := f.Open(); r.IsErr() {
			return r
		}
	}

	if _, err := f.file.Seek(offset, whence); err != nil {
		f.Close()
		return Err[*File](err)
	}

	return Ok(f)
}

// Close closes the File and unlocks its underlying file, if it is not already closed.
func (f *File) Close() error {
	if f.file == nil {
		return &ErrFileClosed{f.name.Std()}
	}

	var err error

	if f.guard {
		err = filelock.Unlock(f.file)
	}

	if closeErr := f.file.Close(); closeErr != nil {
		err = closeErr
	}

	f.file = nil

	return err
}

// Copy copies the file to the specified destination, with the specified mode (optional).
// If no mode is provided, the default FileMode (0644) is used.
func (f *File) Copy(dest String, mode ...os.FileMode) Result[*File] {
	if r := f.Open(); r.IsErr() {
		return r
	}

	defer f.Close()

	nf := NewFile(dest)
	if f.guard {
		nf.guard = true
	}

	return nf.WriteFromReader(f.file, mode...)
}

// Create is similar to os.Create, but it returns a write-locked file.
// Don't forget to close the file!
func (f *File) Create() Result[*File] {
	return f.OpenFile(os.O_RDWR|os.O_CREATE|os.O_TRUNC, FileCreate)
}

// Dir returns the directory the file is in as an Dir instance.
func (f *File) Dir() Result[*Dir] {
	dirPath := f.dirPath()
	if dirPath.IsErr() {
		return Err[*Dir](dirPath.err)
	}

	return Ok(NewDir(dirPath.v))
}

// Exist checks if the file exists.
func (f *File) Exist() bool {
	if f.dirPath().IsOk() {
		filePath := f.filePath()
		if filePath.IsOk() {
			_, err := os.Stat(filePath.v.Std())
			return !os.IsNotExist(err)
		}
	}

	return false
}

// Ext returns the file extension.
func (f *File) Ext() String { return String(filepath.Ext(f.name.Std())) }

// Guard sets a lock on the file to protect it from concurrent access.
// It returns the File instance with the guard enabled.
func (f *File) Guard() *File {
	f.guard = true
	return f
}

// MimeType returns the MIME type of the file as Result[String].
func (f *File) MimeType() Result[String] {
	if r := f.Open(); r.IsErr() {
		return Err[String](r.err)
	}

	defer f.Close()

	const bufferSize = 512

	buff := make([]byte, bufferSize)

	bytesRead, err := f.file.ReadAt(buff, 0)
	if err != nil && err != io.EOF {
		return Err[String](err)
	}

	buff = buff[:bytesRead]

	return Ok(String(http.DetectContentType(buff)))
}

// Move function simply calls [File.Rename]
func (f *File) Move(newpath String) Result[*File] { return f.Rename(newpath) }

// Name returns the name of the file.
func (f *File) Name() String {
	if f.file != nil {
		return String(filepath.Base(f.file.Name()))
	}

	return String(filepath.Base(f.name.Std()))
}

// Open is like os.Open, but returns a read-locked file.
// Don't forget to close the file!
func (f *File) Open() Result[*File] { return f.OpenFile(os.O_RDONLY, 0) }

// OpenFile is like os.OpenFile, but returns a locked file.
// If flag includes os.O_WRONLY or os.O_RDWR, the file is write-locked
// otherwise, it is read-locked.
// Don't forget to close the file!
func (f *File) OpenFile(flag int, perm fs.FileMode) Result[*File] {
	file, err := os.OpenFile(f.name.Std(), flag&^os.O_TRUNC, perm)
	if err != nil {
		return Err[*File](err)
	}

	if f.guard {
		switch flag & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR) {
		case os.O_WRONLY, os.O_RDWR:
			err = filelock.Lock(file)
		default:
			err = filelock.RLock(file)
		}

		if err != nil {
			file.Close()
			return Err[*File](err)
		}
	}

	if flag&os.O_TRUNC == os.O_TRUNC {
		if err := file.Truncate(0); err != nil {
			if fi, statErr := file.Stat(); statErr != nil || fi.Mode().IsRegular() {
				if f.guard {
					filelock.Unlock(file)
				}

				file.Close()

				return Err[*File](err)
			}
		}
	}

	f.file = file

	return Ok(f)
}

// Path returns the absolute path of the file.
func (f *File) Path() Result[String] { return f.filePath() }

// Print writes the content of the File to the standard output (console)
// and returns the File unchanged.
func (f *File) Print() *File { fmt.Print(f); return f }

// Println writes the content of the File to the standard output (console) with a newline
// and returns the File unchanged.
func (f *File) Println() *File { fmt.Println(f); return f }

// Read opens the named file with a read-lock and returns its contents.
func (f *File) Read() Result[String] {
	if r := f.Open(); r.IsErr() {
		return Err[String](r.err)
	}

	defer f.Close()

	content, err := io.ReadAll(f.file)
	if err != nil {
		return Err[String](err)
	}

	return Ok(String(content))
}

// Remove removes the file.
func (f *File) Remove() Result[*File] {
	if err := os.Remove(f.name.Std()); err != nil {
		return Err[*File](err)
	}

	return Ok(f)
}

// Rename renames the file to the specified new path.
func (f *File) Rename(newpath String) Result[*File] {
	if !f.Exist() {
		return Err[*File](&ErrFileNotExist{f.name.Std()})
	}

	nf := NewFile(newpath)
	if f.guard {
		nf.guard = true
	}

	if r := nf.createAll(); r.IsErr() {
		return r
	}

	if err := os.Rename(f.name.Std(), newpath.Std()); err != nil {
		return Err[*File](err)
	}

	return Ok(nf)
}

// Split splits the file path into its directory and file components.
func (f *File) Split() (*Dir, *File) {
	path := f.Path()
	if path.IsErr() {
		return nil, nil
	}

	dir, file := filepath.Split(path.v.Std())

	return NewDir(String(dir)), NewFile(String(file))
}

// Stat returns the fs.FileInfo of the file.
// It calls the file's Stat method if the file is open, or os.Stat otherwise.
func (f *File) Stat() Result[fs.FileInfo] {
	if f.file != nil {
		return ResultOf(f.file.Stat())
	}

	return ResultOf(os.Stat(f.name.Std()))
}

// Lstat retrieves information about the symbolic link represented by the *File instance.
// It returns a Result[fs.FileInfo] containing details about the symbolic link's metadata.
// Unlike Stat, Lstat does not follow the link and provides information about the link itself.
func (f *File) Lstat() Result[fs.FileInfo] {
	return ResultOf(os.Lstat(f.name.Std()))
}

// IsDir checks if the file is a directory.
func (f *File) IsDir() bool {
	stat := f.Stat()
	return stat.IsOk() && stat.v.IsDir()
}

// IsLink checks if the file is a symbolic link.
func (f *File) IsLink() bool {
	stat := f.Lstat()
	return stat.IsOk() && stat.v.Mode()&os.ModeSymlink != 0
}

// Std returns the underlying *os.File instance.
// Don't forget to close the file with g.File().Close()!
func (f *File) Std() *os.File { return f.file }

// CreateTemp creates a new temporary file in the specified directory with the
// specified name pattern and returns a Result, which contains a pointer to the File
// or an error if the operation fails.
// If no directory is specified, the default directory for temporary files is used.
// If no name pattern is specified, the default pattern "*" is used.
//
// Parameters:
//
// - args ...String: A variadic parameter specifying the directory and/or name
// pattern for the temporary file.
//
// Returns:
//
// - *File: A pointer to the File representing the temporary file.
//
// Example usage:
//
//	f := g.NewFile("")
//	tmpfile := f.CreateTemp()                     // Creates a temporary file with default settings
//	tmpfileWithDir := f.CreateTemp("mydir")       // Creates a temporary file in "mydir" directory
//	tmpfileWithPattern := f.CreateTemp("", "tmp") // Creates a temporary file with "tmp" pattern
func (f *File) CreateTemp(args ...String) Result[*File] {
	dir := ""
	pattern := "*"

	if len(args) != 0 {
		if len(args) > 1 {
			pattern = args[1].Std()
		}

		dir = args[0].Std()
	}

	tmpfile, err := os.CreateTemp(dir, pattern)
	if err != nil {
		return Err[*File](err)
	}

	ntmpfile := NewFile(String(tmpfile.Name()))
	ntmpfile.file = tmpfile
	if f.guard {
		ntmpfile.guard = true
	}

	defer ntmpfile.Close()

	return Ok(ntmpfile)
}

// Write opens the named file (creating it with the given permissions if needed),
// then write-locks it and overwrites it with the given content.
func (f *File) Write(content String, mode ...os.FileMode) Result[*File] {
	return f.WriteFromReader(content.Reader(), mode...)
}

// WriteFromReader takes an io.Reader (scr) as input and writes the data from the reader into the file.
// If no FileMode is provided, the default FileMode (0644) is used.
func (f *File) WriteFromReader(scr io.Reader, mode ...os.FileMode) Result[*File] {
	if f.file == nil {
		if r := f.createAll(); r.IsErr() {
			return r
		}
	}

	filePath := f.filePath()
	if filePath.IsErr() {
		return Err[*File](filePath.err)
	}

	fmode := Slice[os.FileMode](mode).Get(0).UnwrapOr(FileDefault)

	if r := f.OpenFile(os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fmode); r.IsErr() {
		return Err[*File](r.err)
	}

	defer f.Close()

	_, err := io.Copy(f.file, scr)
	if err != nil {
		return Err[*File](err)
	}

	err = f.file.Sync()
	if err != nil {
		return Err[*File](err)
	}

	return Ok(f)
}

// dirPath returns the absolute path of the directory containing the file.
func (f *File) dirPath() Result[String] {
	var (
		path string
		err  error
	)

	if f.IsDir() {
		path, err = filepath.Abs(f.name.Std())
	} else {
		path, err = filepath.Abs(filepath.Dir(f.name.Std()))
	}

	if err != nil {
		return Err[String](err)
	}

	return Ok(String(path))
}

// filePath returns the full file path, including the directory and file name.
func (f *File) filePath() Result[String] {
	dirPath := f.dirPath()
	if dirPath.IsErr() {
		return Err[String](dirPath.err)
	}

	if f.IsDir() {
		return dirPath
	}

	return Ok(String(filepath.Join(dirPath.v.Std(), filepath.Base(f.name.Std()))))
}

func (f *File) createAll() Result[*File] {
	dirPath := f.dirPath()
	if dirPath.IsErr() {
		return Err[*File](dirPath.err)
	}

	if !f.Exist() {
		if err := os.MkdirAll(dirPath.v.Std(), DirDefault); err != nil {
			return Err[*File](err)
		}
	}

	return Ok(f)
}
