/*
Copyright 2017 The Kubernetes 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 openapi

import (
	"bytes"
	"encoding/gob"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"

	"github.com/golang/glog"

	"k8s.io/client-go/discovery"
	"k8s.io/kubernetes/pkg/version"
)

func init() {
	registerBinaryEncodingTypes()
}

const openapiFileName = "openapi_cache"

type CachingOpenAPIClient struct {
	version      string
	client       discovery.OpenAPISchemaInterface
	cacheDirName string
}

// NewCachingOpenAPIClient returns a new discovery.OpenAPISchemaInterface
// that will read the openapi spec from a local cache if it exists, and
// if not will then fetch an openapi spec using a client.
// client: used to fetch a new openapi spec if a local cache is not found
// version: the server version and used as part of the cache file location
// cacheDir: the directory under which the cache file will be written
func NewCachingOpenAPIClient(client discovery.OpenAPISchemaInterface, version, cacheDir string) *CachingOpenAPIClient {
	return &CachingOpenAPIClient{
		client:       client,
		version:      version,
		cacheDirName: cacheDir,
	}
}

// OpenAPIData returns an openapi spec.
// It will first attempt to read the spec from a local cache
// If it cannot read a local cache, it will read the file
// using the client and then write the cache.
func (c *CachingOpenAPIClient) OpenAPIData() (*Resources, error) {
	// Try to use the cached version
	if c.useCache() {
		doc, err := c.readOpenAPICache()
		if err == nil {
			return doc, nil
		}
	}

	// No cached version found, download from server
	s, err := c.client.OpenAPISchema()
	if err != nil {
		glog.V(2).Infof("Failed to download openapi data %v", err)
		return nil, err
	}

	oa, err := NewOpenAPIData(s)
	if err != nil {
		glog.V(2).Infof("Failed to parse openapi data %v", err)
		return nil, err
	}

	// Try to cache the openapi spec
	if c.useCache() {
		err = c.writeToCache(oa)
		if err != nil {
			// Just log an message, no need to fail the command since we got the data we need
			glog.V(2).Infof("Unable to cache openapi spec %v", err)
		}
	}

	// Return the parsed data
	return oa, nil
}

// useCache returns true if the client should try to use the cache file
func (c *CachingOpenAPIClient) useCache() bool {
	return len(c.version) > 0 && len(c.cacheDirName) > 0
}

// readOpenAPICache tries to read the openapi spec from the local file cache
func (c *CachingOpenAPIClient) readOpenAPICache() (*Resources, error) {
	// Get the filename to read
	filename := c.openAPICacheFilename()

	// Read the cached file
	data, err := ioutil.ReadFile(filename)
	if err != nil {
		return nil, err
	}

	// Decode the openapi spec
	s, err := c.decodeSpec(data)

	return s, err
}

// decodeSpec binary decodes the openapi spec
func (c *CachingOpenAPIClient) decodeSpec(data []byte) (*Resources, error) {
	b := bytes.NewBuffer(data)
	d := gob.NewDecoder(b)
	parsed := &Resources{}
	err := d.Decode(parsed)
	return parsed, err
}

// encodeSpec binary encodes the openapi spec
func (c *CachingOpenAPIClient) encodeSpec(parsed *Resources) ([]byte, error) {
	b := &bytes.Buffer{}
	e := gob.NewEncoder(b)
	err := e.Encode(parsed)
	return b.Bytes(), err

}

// writeToCache tries to write the openapi spec to the local file cache.
// writes the data to a new tempfile, and then links the cache file and the tempfile
func (c *CachingOpenAPIClient) writeToCache(parsed *Resources) error {
	// Get the constant filename used to read the cache.
	cacheFile := c.openAPICacheFilename()

	// Binary encode the spec.  This is 10x as fast as using json encoding.  (60ms vs 600ms)
	b, err := c.encodeSpec(parsed)
	if err != nil {
		return fmt.Errorf("Could not binary encode openapi spec: %v", err)
	}

	// Create a new temp file for the cached openapi spec.
	cacheDir := filepath.Dir(cacheFile)
	if err := os.MkdirAll(cacheDir, 0755); err != nil {
		return fmt.Errorf("Could not create directory: %v %v", cacheDir, err)
	}
	tmpFile, err := ioutil.TempFile(cacheDir, "openapi")
	if err != nil {
		return fmt.Errorf("Could not create temp cache file: %v %v", cacheFile, err)
	}

	// Write the binary encoded openapi spec to the temp file
	if _, err := io.Copy(tmpFile, bytes.NewBuffer(b)); err != nil {
		return fmt.Errorf("Could not write temp cache file: %v", err)
	}

	// Link the temp cache file to the constant cache filepath
	return linkFiles(tmpFile.Name(), cacheFile)
}

// openAPICacheFilename returns the filename to read the cache from
func (c *CachingOpenAPIClient) openAPICacheFilename() string {
	// Cache using the client and server versions
	return filepath.Join(c.cacheDirName, c.version, version.Get().GitVersion, openapiFileName)
}

// linkFiles links the old file to the new file
func linkFiles(old, new string) error {
	if err := os.Link(old, new); err != nil {
		// If we can't write due to file existing, or permission problems, keep going.
		if os.IsExist(err) || os.IsPermission(err) {
			return nil
		}
		return err
	}
	return nil
}

// registerBinaryEncodingTypes registers the types so they can be binary encoded by gob
func registerBinaryEncodingTypes() {
	gob.Register(map[string]interface{}{})
	gob.Register([]interface{}{})
	gob.Register(Resources{})
}
