/*
   Copyright The containerd Authors.

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package continuity

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"

	"github.com/containerd/continuity/devices"
	driverpkg "github.com/containerd/continuity/driver"
	"github.com/containerd/continuity/pathdriver"

	"github.com/opencontainers/go-digest"
)

var (
	// ErrNotFound represents the resource not found
	ErrNotFound = fmt.Errorf("not found")
	// ErrNotSupported represents the resource not supported
	ErrNotSupported = fmt.Errorf("not supported")
)

// Context represents a file system context for accessing resources. The
// responsibility of the context is to convert system specific resources to
// generic Resource objects. Most of this is safe path manipulation, as well
// as extraction of resource details.
type Context interface {
	Apply(Resource) error
	Verify(Resource) error
	Resource(string, os.FileInfo) (Resource, error)
	Walk(filepath.WalkFunc) error
}

// SymlinkPath is intended to give the symlink target value
// in a root context. Target and linkname are absolute paths
// not under the given root.
type SymlinkPath func(root, linkname, target string) (string, error)

// ContextOptions represents options to create a new context.
type ContextOptions struct {
	Digester   Digester
	Driver     driverpkg.Driver
	PathDriver pathdriver.PathDriver
	Provider   ContentProvider
}

// context represents a file system context for accessing resources.
// Generally, all path qualified access and system considerations should land
// here.
type context struct {
	driver     driverpkg.Driver
	pathDriver pathdriver.PathDriver
	root       string
	digester   Digester
	provider   ContentProvider
}

// NewContext returns a Context associated with root. The default driver will
// be used, as returned by NewDriver.
func NewContext(root string) (Context, error) {
	return NewContextWithOptions(root, ContextOptions{})
}

// NewContextWithOptions returns a Context associate with the root.
func NewContextWithOptions(root string, options ContextOptions) (Context, error) {
	// normalize to absolute path
	pathDriver := options.PathDriver
	if pathDriver == nil {
		pathDriver = pathdriver.LocalPathDriver
	}

	root = pathDriver.FromSlash(root)
	root, err := pathDriver.Abs(pathDriver.Clean(root))
	if err != nil {
		return nil, err
	}

	driver := options.Driver
	if driver == nil {
		driver, err = driverpkg.NewSystemDriver()
		if err != nil {
			return nil, err
		}
	}

	digester := options.Digester
	if digester == nil {
		digester = simpleDigester{digest.Canonical}
	}

	// Check the root directory. Need to be a little careful here. We are
	// allowing a link for now, but this may have odd behavior when
	// canonicalizing paths. As long as all files are opened through the link
	// path, this should be okay.
	fi, err := driver.Stat(root)
	if err != nil {
		return nil, err
	}

	if !fi.IsDir() {
		return nil, &os.PathError{Op: "NewContext", Path: root, Err: os.ErrInvalid}
	}

	return &context{
		root:       root,
		driver:     driver,
		pathDriver: pathDriver,
		digester:   digester,
		provider:   options.Provider,
	}, nil
}

// Resource returns the resource as path p, populating the entry with info
// from fi. The path p should be the path of the resource in the context,
// typically obtained through Walk or from the value of Resource.Path(). If fi
// is nil, it will be resolved.
func (c *context) Resource(p string, fi os.FileInfo) (Resource, error) {
	fp, err := c.fullpath(p)
	if err != nil {
		return nil, err
	}

	if fi == nil {
		fi, err = c.driver.Lstat(fp)
		if err != nil {
			return nil, err
		}
	}

	base, err := newBaseResource(p, fi)
	if err != nil {
		return nil, err
	}

	base.xattrs, err = c.resolveXAttrs(fp, fi, base)
	if err != nil && !errors.Is(err, ErrNotSupported) {
		return nil, err
	}

	// TODO(stevvooe): Handle windows alternate data streams.

	if fi.Mode().IsRegular() {
		dgst, err := c.digest(p)
		if err != nil {
			return nil, err
		}

		return newRegularFile(*base, base.paths, fi.Size(), dgst)
	}

	if fi.Mode().IsDir() {
		return newDirectory(*base)
	}

	if fi.Mode()&os.ModeSymlink != 0 {
		// We handle relative links vs absolute links by including a
		// beginning slash for absolute links. Effectively, the bundle's
		// root is treated as the absolute link anchor.
		target, err := c.driver.Readlink(fp)
		if err != nil {
			return nil, err
		}

		return newSymLink(*base, target)
	}

	if fi.Mode()&os.ModeNamedPipe != 0 {
		return newNamedPipe(*base, base.paths)
	}

	if fi.Mode()&os.ModeDevice != 0 {
		deviceDriver, ok := c.driver.(driverpkg.DeviceInfoDriver)
		if !ok {
			return nil, fmt.Errorf("device extraction is not supported for %s: %w", fp, ErrNotSupported)
		}

		// character and block devices merely need to recover the
		// major/minor device number.
		major, minor, err := deviceDriver.DeviceInfo(fi)
		if err != nil {
			return nil, err
		}

		return newDevice(*base, base.paths, major, minor)
	}

	return nil, fmt.Errorf("%q (%v) is not supported: %w", fp, fi.Mode(), ErrNotFound)
}

func (c *context) verifyMetadata(resource, target Resource) error {
	if target.Mode() != resource.Mode() {
		return fmt.Errorf("resource %q has incorrect mode: %v != %v", target.Path(), target.Mode(), resource.Mode())
	}

	if target.UID() != resource.UID() {
		return fmt.Errorf("unexpected uid for %q: %v != %v", target.Path(), target.UID(), resource.GID())
	}

	if target.GID() != resource.GID() {
		return fmt.Errorf("unexpected gid for %q: %v != %v", target.Path(), target.GID(), target.GID())
	}

	if xattrer, ok := resource.(XAttrer); ok {
		txattrer, tok := target.(XAttrer)
		if !tok {
			return fmt.Errorf("resource %q has xattrs but target does not support them", resource.Path())
		}

		// For xattrs, only ensure that we have those defined in the resource
		// and their values match. We can ignore other xattrs. In other words,
		// we only verify that target has the subset defined by resource.
		txattrs := txattrer.XAttrs()
		for attr, value := range xattrer.XAttrs() {
			tvalue, ok := txattrs[attr]
			if !ok {
				return fmt.Errorf("resource %q target missing xattr %q", resource.Path(), attr)
			}

			if !bytes.Equal(value, tvalue) {
				return fmt.Errorf("xattr %q value differs for resource %q", attr, resource.Path())
			}
		}
	}

	switch r := resource.(type) {
	case RegularFile:
		// TODO(stevvooe): Another reason to use a record-based approach. We
		// have to do another type switch to get this to work. This could be
		// fixed with an Equal function, but let's study this a little more to
		// be sure.
		t, ok := target.(RegularFile)
		if !ok {
			return fmt.Errorf("resource %q target not a regular file", r.Path())
		}

		if t.Size() != r.Size() {
			return fmt.Errorf("resource %q target has incorrect size: %v != %v", t.Path(), t.Size(), r.Size())
		}
	case Directory:
		t, ok := target.(Directory)
		if !ok {
			return fmt.Errorf("resource %q target not a directory", t.Path())
		}
	case SymLink:
		t, ok := target.(SymLink)
		if !ok {
			return fmt.Errorf("resource %q target not a symlink", t.Path())
		}

		if t.Target() != r.Target() {
			return fmt.Errorf("resource %q target has mismatched target: %q != %q", t.Path(), t.Target(), r.Target())
		}
	case Device:
		t, ok := target.(Device)
		if !ok {
			return fmt.Errorf("resource %q is not a device", t.Path())
		}

		if t.Major() != r.Major() || t.Minor() != r.Minor() {
			return fmt.Errorf("resource %q has mismatched major/minor numbers: %d,%d != %d,%d", t.Path(), t.Major(), t.Minor(), r.Major(), r.Minor())
		}
	case NamedPipe:
		t, ok := target.(NamedPipe)
		if !ok {
			return fmt.Errorf("resource %q is not a named pipe", t.Path())
		}
	default:
		return fmt.Errorf("cannot verify resource: %v", resource)
	}

	return nil
}

// Verify the resource in the context. An error will be returned a discrepancy
// is found.
func (c *context) Verify(resource Resource) error {
	fp, err := c.fullpath(resource.Path())
	if err != nil {
		return err
	}

	fi, err := c.driver.Lstat(fp)
	if err != nil {
		return err
	}

	target, err := c.Resource(resource.Path(), fi)
	if err != nil {
		return err
	}

	if target.Path() != resource.Path() {
		return fmt.Errorf("resource paths do not match: %q != %q", target.Path(), resource.Path())
	}

	if err := c.verifyMetadata(resource, target); err != nil {
		return err
	}

	if h, isHardlinkable := resource.(Hardlinkable); isHardlinkable {
		hardlinkKey, err := newHardlinkKey(fi)
		if err == errNotAHardLink {
			if len(h.Paths()) > 1 {
				return fmt.Errorf("%q is not a hardlink to %q", h.Paths()[1], resource.Path())
			}
		} else if err != nil {
			return err
		}

		for _, path := range h.Paths()[1:] {
			fpLink, err := c.fullpath(path)
			if err != nil {
				return err
			}

			fiLink, err := c.driver.Lstat(fpLink)
			if err != nil {
				return err
			}

			targetLink, err := c.Resource(path, fiLink)
			if err != nil {
				return err
			}

			hardlinkKeyLink, err := newHardlinkKey(fiLink)
			if err != nil {
				return err
			}

			if hardlinkKeyLink != hardlinkKey {
				return fmt.Errorf("%q is not a hardlink to %q", path, resource.Path())
			}

			if err := c.verifyMetadata(resource, targetLink); err != nil {
				return err
			}
		}
	}

	switch r := resource.(type) {
	case RegularFile:
		t, ok := target.(RegularFile)
		if !ok {
			return fmt.Errorf("resource %q target not a regular file", r.Path())
		}

		// TODO(stevvooe): This may need to get a little more sophisticated
		// for digest comparison. We may want to actually calculate the
		// provided digests, rather than the implementations having an
		// overlap.
		if !digestsMatch(t.Digests(), r.Digests()) {
			return fmt.Errorf("digests for resource %q do not match: %v != %v", t.Path(), t.Digests(), r.Digests())
		}
	}

	return nil
}

func (c *context) checkoutFile(fp string, rf RegularFile) error {
	if c.provider == nil {
		return fmt.Errorf("no file provider")
	}
	var (
		r   io.ReadCloser
		err error
	)
	for _, dgst := range rf.Digests() {
		r, err = c.provider.Reader(dgst)
		if err == nil {
			break
		}
	}
	if err != nil {
		return fmt.Errorf("file content could not be provided: %w", err)
	}
	defer r.Close()

	return atomicWriteFile(fp, r, rf.Size(), rf.Mode())
}

// Apply the resource to the contexts. An error will be returned if the
// operation fails. Depending on the resource type, the resource may be
// created. For resource that cannot be resolved, an error will be returned.
func (c *context) Apply(resource Resource) error {
	fp, err := c.fullpath(resource.Path())
	if err != nil {
		return err
	}

	if !strings.HasPrefix(fp, c.root) {
		return fmt.Errorf("resource %v escapes root", resource)
	}

	chmod := true
	fi, err := c.driver.Lstat(fp)
	if err != nil {
		if !os.IsNotExist(err) {
			return err
		}
	}

	switch r := resource.(type) {
	case RegularFile:
		if fi == nil {
			if err := c.checkoutFile(fp, r); err != nil {
				return fmt.Errorf("error checking out file %q: %w", resource.Path(), err)
			}
			chmod = false
		} else {
			if !fi.Mode().IsRegular() {
				return fmt.Errorf("file %q should be a regular file, but is not", resource.Path())
			}
			if fi.Size() != r.Size() {
				if err := c.checkoutFile(fp, r); err != nil {
					return fmt.Errorf("error checking out file %q: %w", resource.Path(), err)
				}
			} else {
				for _, dgst := range r.Digests() {
					f, err := os.Open(fp)
					if err != nil {
						return fmt.Errorf("failure opening file for read %q: %w", resource.Path(), err)
					}
					compared, err := dgst.Algorithm().FromReader(f)
					if err == nil && dgst != compared {
						if err := c.checkoutFile(fp, r); err != nil {
							return fmt.Errorf("error checking out file %q: %w", resource.Path(), err)
						}
						break
					}
					if err1 := f.Close(); err == nil {
						err = err1
					}
					if err != nil {
						return fmt.Errorf("error checking digest for %q: %w", resource.Path(), err)
					}
				}
			}
		}
	case Directory:
		if fi == nil {
			if err := c.driver.Mkdir(fp, resource.Mode()); err != nil {
				return err
			}
		} else if !fi.Mode().IsDir() {
			return fmt.Errorf("%q should be a directory, but is not", resource.Path())
		}

	case SymLink:
		var target string // only possibly set if target resource is a symlink

		if fi != nil {
			if fi.Mode()&os.ModeSymlink != 0 {
				target, err = c.driver.Readlink(fp)
				if err != nil {
					return err
				}
			}
		}

		if target != r.Target() {
			if fi != nil {
				if err := c.driver.Remove(fp); err != nil { // RemoveAll in case of directory?
					return err
				}
			}

			if err := c.driver.Symlink(r.Target(), fp); err != nil {
				return err
			}
		}

	case Device:
		if fi == nil {
			if err := c.driver.Mknod(fp, resource.Mode(), int(r.Major()), int(r.Minor())); err != nil {
				return err
			}
		} else if (fi.Mode() & os.ModeDevice) == 0 {
			return fmt.Errorf("%q should be a device, but is not", resource.Path())
		} else {
			major, minor, err := devices.DeviceInfo(fi)
			if err != nil {
				return err
			}
			if major != r.Major() || minor != r.Minor() {
				if err := c.driver.Remove(fp); err != nil {
					return err
				}

				if err := c.driver.Mknod(fp, resource.Mode(), int(r.Major()), int(r.Minor())); err != nil {
					return err
				}
			}
		}

	case NamedPipe:
		if fi == nil {
			if err := c.driver.Mkfifo(fp, resource.Mode()); err != nil {
				return err
			}
		} else if (fi.Mode() & os.ModeNamedPipe) == 0 {
			return fmt.Errorf("%q should be a named pipe, but is not", resource.Path())
		}
	}

	if h, isHardlinkable := resource.(Hardlinkable); isHardlinkable {
		for _, path := range h.Paths() {
			if path == resource.Path() {
				continue
			}

			lp, err := c.fullpath(path)
			if err != nil {
				return err
			}

			if _, fi := c.driver.Lstat(lp); fi == nil {
				c.driver.Remove(lp)
			}
			if err := c.driver.Link(fp, lp); err != nil {
				return err
			}
		}
	}

	// Update filemode if file was not created
	if chmod {
		if err := c.driver.Lchmod(fp, resource.Mode()); err != nil {
			return err
		}
	}

	if err := c.driver.Lchown(fp, resource.UID(), resource.GID()); err != nil {
		return err
	}

	if xattrer, ok := resource.(XAttrer); ok {
		// For xattrs, only ensure that we have those defined in the resource
		// and their values are set. We can ignore other xattrs. In other words,
		// we only set xattres defined by resource but never remove.

		if _, ok := resource.(SymLink); ok {
			lxattrDriver, ok := c.driver.(driverpkg.LXAttrDriver)
			if !ok {
				return fmt.Errorf("unsupported symlink xattr for resource %q", resource.Path())
			}
			if err := lxattrDriver.LSetxattr(fp, xattrer.XAttrs()); err != nil {
				return err
			}
		} else {
			xattrDriver, ok := c.driver.(driverpkg.XAttrDriver)
			if !ok {
				return fmt.Errorf("unsupported xattr for resource %q", resource.Path())
			}
			if err := xattrDriver.Setxattr(fp, xattrer.XAttrs()); err != nil {
				return err
			}
		}
	}

	return nil
}

// Walk provides a convenience function to call filepath.Walk correctly for
// the context. Otherwise identical to filepath.Walk, the path argument is
// corrected to be contained within the context.
func (c *context) Walk(fn filepath.WalkFunc) error {
	root := c.root
	fi, err := c.driver.Lstat(c.root)
	if err == nil && fi.Mode()&os.ModeSymlink != 0 {
		root, err = c.driver.Readlink(c.root)
		if err != nil {
			return err
		}
	}
	return c.pathDriver.Walk(root, func(p string, fi os.FileInfo, _ error) error {
		contained, err := c.containWithRoot(p, root)
		return fn(contained, fi, err)
	})
}

// fullpath returns the system path for the resource, joined with the context
// root. The path p must be a part of the context.
func (c *context) fullpath(p string) (string, error) {
	p = c.pathDriver.Join(c.root, p)
	if !strings.HasPrefix(p, c.root) {
		return "", fmt.Errorf("invalid context path")
	}

	return p, nil
}

// containWithRoot cleans and santizes the filesystem path p to be an absolute path,
// effectively relative to the passed root. Extra care should be used when calling this
// instead of contain. This is needed for Walk, as if context root is a symlink,
// it must be evaluated prior to the Walk
func (c *context) containWithRoot(p string, root string) (string, error) {
	sanitized, err := c.pathDriver.Rel(root, p)
	if err != nil {
		return "", err
	}

	// ZOMBIES(stevvooe): In certain cases, we may want to remap these to a
	// "containment error", so the caller can decide what to do.
	return c.pathDriver.Join("/", c.pathDriver.Clean(sanitized)), nil
}

// digest returns the digest of the file at path p, relative to the root.
func (c *context) digest(p string) (digest.Digest, error) {
	f, err := c.driver.Open(c.pathDriver.Join(c.root, p))
	if err != nil {
		return "", err
	}
	defer f.Close()

	return c.digester.Digest(f)
}

// resolveXAttrs attempts to resolve the extended attributes for the resource
// at the path fp, which is the full path to the resource. If the resource
// cannot have xattrs, nil will be returned.
func (c *context) resolveXAttrs(fp string, fi os.FileInfo, base *resource) (map[string][]byte, error) {
	if fi.Mode().IsRegular() || fi.Mode().IsDir() {
		xattrDriver, ok := c.driver.(driverpkg.XAttrDriver)
		if !ok {
			return nil, fmt.Errorf("xattr extraction is not supported: %w", ErrNotSupported)
		}

		return xattrDriver.Getxattr(fp)
	}

	if fi.Mode()&os.ModeSymlink != 0 {
		lxattrDriver, ok := c.driver.(driverpkg.LXAttrDriver)
		if !ok {
			return nil, fmt.Errorf("xattr extraction for symlinks is not supported: %w", ErrNotSupported)
		}

		return lxattrDriver.LGetxattr(fp)
	}

	return nil, nil
}
