//  Copyright 2020 Google Inc. All Rights Reserved.
//
//  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 config

import (
	"context"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/GoogleCloudPlatform/osconfig/clog"
	"github.com/GoogleCloudPlatform/osconfig/util"

	"cloud.google.com/go/osconfig/agentendpoint/apiv1/agentendpointpb"
)

const defaultFilePerms = 0644

type fileResource struct {
	*agentendpointpb.OSPolicy_Resource_FileResource

	managedFile ManagedFile
}

// ManagedFile is the file that this FileResouce manages.
type ManagedFile struct {
	Path       string
	tempDir    string
	source     string
	checksum   string
	State      agentendpointpb.OSPolicy_Resource_FileResource_DesiredState
	Permisions os.FileMode
}

func parsePermissions(s string) (os.FileMode, error) {
	if s == "" {
		return defaultFilePerms, nil
	}

	i, err := strconv.ParseUint(s, 8, 32)
	if err != nil {
		return 0, err
	}
	return os.FileMode(i), nil
}

// TODO: use a persistent cache for downloaded files so we dont need to redownload them each time.
func (f *fileResource) download(ctx context.Context) error {
	// No need to download if source is a local file.
	if f.GetFile().GetLocalPath() != "" {
		return nil
	}

	tmpDir, err := ioutil.TempDir("", "osconfig_file_resource_")
	if err != nil {
		return fmt.Errorf("failed to create working dir: %s", err)
	}
	f.managedFile.tempDir = tmpDir

	tmpFile := filepath.Join(tmpDir, filepath.Base(f.GetPath()))
	f.managedFile.source = tmpFile
	perms := os.FileMode(0644)

	switch f.GetSource().(type) {
	case *agentendpointpb.OSPolicy_Resource_FileResource_Content:
		f.managedFile.checksum, err = util.AtomicWriteFileStream(strings.NewReader(f.GetContent()), "", tmpFile, perms)
		if err != nil {
			return err
		}

	case *agentendpointpb.OSPolicy_Resource_FileResource_File:
		f.managedFile.checksum, err = downloadFile(ctx, tmpFile, perms, f.GetFile())
		if err != nil {
			return err
		}
	default:
		return fmt.Errorf("unrecognized Source type for FileResource: %q", f.GetSource())
	}

	return nil
}

func (f *fileResource) validate(ctx context.Context) (*ManagedResources, error) {
	switch f.GetState() {
	case agentendpointpb.OSPolicy_Resource_FileResource_ABSENT, agentendpointpb.OSPolicy_Resource_FileResource_PRESENT, agentendpointpb.OSPolicy_Resource_FileResource_CONTENTS_MATCH:
		f.managedFile.State = f.GetState()
	default:
		return nil, fmt.Errorf("unrecognized DesiredState for FileResource: %q", f.GetState())
	}

	f.managedFile.Path = f.GetPath()

	// If desired state is absent, we can return now.
	if f.GetState() == agentendpointpb.OSPolicy_Resource_FileResource_ABSENT {
		return &ManagedResources{Files: []ManagedFile{f.managedFile}}, nil
	}

	perms, err := parsePermissions(f.GetPermissions())
	if err != nil {
		return nil, fmt.Errorf("can't parse permissions %q: %v", f.GetPermissions(), err)
	}
	f.managedFile.Permisions = perms

	if f.GetFile().GetLocalPath() != "" {
		f.managedFile.source = f.GetFile().GetLocalPath()
		file, err := os.Open(f.GetFile().GetLocalPath())
		if err != nil {
			return nil, err
		}
		f.managedFile.checksum = checksum(file)
		file.Close()
	}

	switch f.managedFile.State {
	case agentendpointpb.OSPolicy_Resource_FileResource_ABSENT:
	case agentendpointpb.OSPolicy_Resource_FileResource_PRESENT:
		// If the file is already present no need to downloaded it.
		if !util.Exists(f.managedFile.Path) {
			if err := f.download(ctx); err != nil {
				return nil, err
			}
		}
	case agentendpointpb.OSPolicy_Resource_FileResource_CONTENTS_MATCH:
		if err := f.download(ctx); err != nil {
			return nil, err
		}
	default:
		return nil, fmt.Errorf("unrecognized DesiredState for FileResource: %q", f.managedFile.State)
	}

	return &ManagedResources{Files: []ManagedFile{f.managedFile}}, nil
}

func (f *fileResource) checkState(ctx context.Context) (inDesiredState bool, err error) {
	switch f.managedFile.State {
	case agentendpointpb.OSPolicy_Resource_FileResource_ABSENT:
		return !util.Exists(f.managedFile.Path), nil
	case agentendpointpb.OSPolicy_Resource_FileResource_PRESENT:
		return util.Exists(f.managedFile.Path), nil
	case agentendpointpb.OSPolicy_Resource_FileResource_CONTENTS_MATCH:
		return contentsMatch(ctx, f.managedFile.Path, f.managedFile.checksum)
	default:
		return false, fmt.Errorf("unrecognized DesiredState for FileResource: %q", f.managedFile.State)
	}
}

func copyFile(dst, src string, perms os.FileMode) (retErr error) {
	reader, err := os.Open(src)
	if err != nil {
		return fmt.Errorf("error opening source file: %v", err)
	}
	defer reader.Close()
	writer, err := os.OpenFile(dst, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, perms)
	if err != nil {
		return fmt.Errorf("error opening destination file: %v", err)
	}
	defer func() {
		if err := writer.Close(); err != nil {
			if retErr == nil {
				retErr = fmt.Errorf("error closing destination file: %v", err)
			}
		}
	}()

	if _, err := io.Copy(writer, reader); err != nil {
		return err
	}

	return nil
}

func (f *fileResource) enforceState(ctx context.Context) (inDesiredState bool, err error) {
	clog.Infof(ctx, "Enforcing state %q for file %q.", f.managedFile.State, f.managedFile.Path)
	switch f.managedFile.State {
	case agentendpointpb.OSPolicy_Resource_FileResource_ABSENT:
		if err := os.Remove(f.managedFile.Path); err != nil {
			return false, fmt.Errorf("error removing %q: %v", f.managedFile.Path, err)
		}
	case agentendpointpb.OSPolicy_Resource_FileResource_PRESENT, agentendpointpb.OSPolicy_Resource_FileResource_CONTENTS_MATCH:
		// Download now if for some reason we got this point and have not.
		if f.managedFile.source == "" {
			if err := f.download(ctx); err != nil {
				return false, err
			}
		}
		if err := copyFile(f.managedFile.Path, f.managedFile.source, f.managedFile.Permisions); err != nil {
			return false, fmt.Errorf("error copying %q to %q: %v", f.managedFile.source, f.managedFile.Path, err)
		}
	default:
		return false, fmt.Errorf("unrecognized DesiredState for FileResource: %q", f.managedFile.State)
	}

	return true, nil
}

func (f *fileResource) populateOutput(rCompliance *agentendpointpb.OSPolicyResourceCompliance) {}

func (f *fileResource) cleanup(ctx context.Context) error {
	if f.managedFile.tempDir != "" {
		return os.RemoveAll(f.managedFile.tempDir)
	}
	return nil
}
