package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"io/fs"
	"log"
	"os"
	"path/filepath"
	"strings"
)

var (
	modelPath  string
	outputPath string
)

func init() {
	flag.StringVar(&modelPath, "m", "", "The `path` of the source API models to search for smithy files.")
	flag.StringVar(&outputPath, "o", "", "The `path` the API models are copied to")
}

func main() {
	flag.Parse()
	if len(modelPath) == 0 || len(outputPath) == 0 {
		flag.PrintDefaults()
		log.Fatalf("model path and output path required")
	}

	srcModels, err := findSmithyModels(modelPath)
	if err != nil {
		log.Fatalf("failed to get models, %v", err)
	}

	for _, model := range srcModels {
		if err = copyModelFile(model); err != nil {
			log.Fatalf("copy failed, %v", err)
		}
	}
}

func copyModelFile(model SourceModel) error {
	srcFile, err := os.Open(model.SrcFilepath)
	if err != nil {
		return fmt.Errorf("failed to open source file %w", err)
	}
	defer srcFile.Close()

	dstFile, err := os.OpenFile(filepath.Join(outputPath, model.DstFilename),
		os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
	if err != nil {
		return fmt.Errorf("failed to create destination file %w", err)
	}

	if _, err = io.Copy(dstFile, srcFile); err != nil {
		return fmt.Errorf("failed to copy model, %w", err)
	}

	if err = dstFile.Close(); err != nil {
		return fmt.Errorf("failed to close destination file, %w", err)
	}

	return nil
}

// SourceModel provides the type for a model that should be copied.
type SourceModel struct {
	SDKID       string
	SrcFilepath string
	DstFilename string
}

func findSmithyModels(modelPath string) (map[string]SourceModel, error) {
	models := map[string]SourceModel{}

	err := filepath.WalkDir(modelPath,
		func(path string, d fs.DirEntry, err error) error {
			if err != nil || d.IsDir() {
				return err
			}

			if filepath.Ext(d.Name()) != ".json" {
				return nil
			}

			f, err := os.Open(path)
			if err != nil {
				return err
			}
			defer f.Close()

			var check SmithyCheck
			if err = json.NewDecoder(f).Decode(&check); err != nil {
				log.Printf("skipping %s file, json but unable to decode, %v",
					path, err)
				return err
			}
			if !strings.HasPrefix(check.Smithy, "1.") && !strings.HasPrefix(check.Smithy, "2.") {
				return nil
			}
			var shapes map[string]SmithyShape
			if err = json.Unmarshal(check.Shapes, &shapes); err != nil {
				return fmt.Errorf("failed to unmarshal smithy model %v, %w",
					path, err)
			}

			for name, shape := range shapes {
				if shape.Type != "service" {
					continue
				}
				if shape.Traits.Service == nil {
					// Ignore services that don't have an SDK id.
					continue
				}
				if len(shape.Traits.Service.SDKID) == 0 {
					return fmt.Errorf("smithy service doesn't have sdkId value %s, %s",
						name, path)
				}

				sdkID := shape.Traits.Service.SDKID
				sdkID = strings.ReplaceAll(sdkID, " ", "-")
				sdkID = strings.ToLower(sdkID)
				if o, ok := models[sdkID]; ok {
					return fmt.Errorf("two smithy models have same sdkId %s, 1:%s 2:%s",
						sdkID, o.SrcFilepath, path)
				}
				// TODO what about two services in same model file?
				models[sdkID] = SourceModel{
					SDKID:       sdkID,
					SrcFilepath: path,
					DstFilename: sdkID + ".json",
				}
			}

			return nil
		},
	)
	if err != nil {
		return nil, err
	}

	return models, nil
}

// SmithyCheck provides initial JSON object deserialization checking for a
// Smithy json model.
type SmithyCheck struct {
	Smithy string          `json:"smithy"`
	Shapes json.RawMessage `json:"shapes"`
}

// SmithyShape provides JSON object deserialization type for a Smithy Shape
type SmithyShape struct {
	Type    string `json:"type"`
	Version string `json:"version"`
	Traits  struct {
		Service *struct {
			SDKID string `json:"sdkId"`
		} `json:"aws.api#service"`
	} `json:"traits"`
}
