package github

import (
	"dataservices/internal/entity"
	"encoding/json"
	"errors"
	"fmt"
	"hash/maphash"
	"net/url"
	"sort"
	"strings"

	"github.com/pmezard/go-difflib/difflib"
)

var seed maphash.Seed

func init() {
	seed = maphash.MakeSeed()
}

type nodeType int

const (
	File = iota
	Directory
)

type FileTreeNode struct {
	Name     string
	NodeType nodeType
	Content  []byte
	Parent   *FileTreeNode
	Children []*FileTreeNode
}

func NewFileNode(name string, content []byte) *FileTreeNode {
	return &FileTreeNode{
		Name:     name,
		NodeType: File,
		Content:  content,
	}
}

func NewDirectoryNode(name string) *FileTreeNode {
	return &FileTreeNode{
		Name:     name,
		NodeType: Directory,
		Children: make([]*FileTreeNode, 0),
	}
}

func (n *FileTreeNode) AppendChild(child *FileTreeNode) error {
	if n.NodeType != Directory {
		return errors.New("only support directory append")
	}
	for _, c := range n.Children {
		if c.Name == child.Name {
			return fmt.Errorf("duplicate node name: %s", c.Name)
		}
	}
	child.Parent = n
	n.Children = append(n.Children, child)
	sort.Slice(n.Children, func(i, j int) bool {
		return n.Children[i].Name < n.Children[j].Name
	})
	return nil
}

func (n *FileTreeNode) AppendRelativePathFile(path string, content []byte) error {
	parts := strings.Split(path, "/")
	cleanParts := make([]string, 0)
	for _, p := range parts {
		if p == "" {
			continue
		}
		cleanParts = append(cleanParts, p)
	}
	return n.appendRelativePathFile(cleanParts, content)
}

func (n *FileTreeNode) appendRelativePathFile(parts []string, content []byte) error {
	if len(parts) == 0 {
		return nil
	} else if len(parts) == 1 {
		return n.AppendChild(NewFileNode(parts[0], content))
	}
	dirName := parts[0]
	var dirNode *FileTreeNode
	for _, child := range n.Children {
		if child.Name == dirName && child.NodeType == Directory {
			dirNode = child
			break
		}
	}
	if dirNode == nil {
		dirNode = NewDirectoryNode(dirName)
		if err := n.AppendChild(dirNode); err != nil {
			return err
		}
	}
	return dirNode.appendRelativePathFile(parts[1:], content)
}

func (n *FileTreeNode) Path() string {
	if n.Parent == nil {
		return ""
	}
	return fmt.Sprintf("%s/%s", n.Parent.Path(), n.Name)
}

func (n *FileTreeNode) contentHash() uint64 {
	if n.NodeType == Directory {
		return 0
	}
	var h maphash.Hash
	h.SetSeed(seed)
	h.Write(n.Content)
	return h.Sum64()
}

// traverse all file type children nodes
func (n *FileTreeNode) TraverseFiles() []*FileTreeNode {
	result := make([]*FileTreeNode, 0)
	n.traverseFiles(&result)
	return result
}

func (n *FileTreeNode) traverseFiles(result *[]*FileTreeNode) {
	if n.NodeType == File {
		*result = append(*result, n)
	} else {
		for _, child := range n.Children {
			child.traverseFiles(result)
		}
	}
}

type FileTreeNodeModified struct {
	Before *FileTreeNode
	After  *FileTreeNode
}

type FileTreeDiff struct {
	Added    []*FileTreeNode
	Deleted  []*FileTreeNode
	Modified []*FileTreeNodeModified
}

func (ftd *FileTreeDiff) ToString() (string, error) {
	result := ""
	for _, added := range ftd.Added {
		path := added.Path()
		diff := difflib.UnifiedDiff{
			A:        []string{},
			B:        difflib.SplitLines(string(added.Content)),
			FromFile: path,
			ToFile:   path,
			Context:  5,
		}
		text, err := difflib.GetUnifiedDiffString(diff)
		if err != nil {
			return "", err
		}
		result += text
	}
	for _, deleted := range ftd.Deleted {
		path := deleted.Path()
		diff := difflib.UnifiedDiff{
			A:        difflib.SplitLines(string(deleted.Content)),
			B:        []string{},
			FromFile: path,
			ToFile:   path,
			Context:  5,
		}
		text, err := difflib.GetUnifiedDiffString(diff)
		if err != nil {
			return "", err
		}
		result += text
	}
	for _, modified := range ftd.Modified {
		path := modified.Before.Path()
		diff := difflib.UnifiedDiff{
			A:        difflib.SplitLines(string(modified.Before.Content)),
			B:        difflib.SplitLines(string(modified.After.Content)),
			FromFile: path,
			ToFile:   path,
			Context:  5,
		}
		text, err := difflib.GetUnifiedDiffString(diff)
		if err != nil {
			return "", err
		}
		result += text
	}
	return result, nil
}

// diff two file tree and return the difference
func (oldNode *FileTreeNode) Diff(newNode *FileTreeNode) *FileTreeDiff {
	if newNode == nil {
		return &FileTreeDiff{Deleted: oldNode.TraverseFiles()}
	}

	added := make([]*FileTreeNode, 0)
	deleted := make([]*FileTreeNode, 0)
	modified := make([]*FileTreeNodeModified, 0)

	if oldNode.NodeType == newNode.NodeType && oldNode.Name == newNode.Name {
		if oldNode.NodeType == File && oldNode.contentHash() != newNode.contentHash() {
			modified = append(modified, &FileTreeNodeModified{Before: oldNode, After: newNode})
		} else if oldNode.NodeType == Directory {
			o, n, O, N := 0, 0, len(oldNode.Children), len(newNode.Children)
			for o < O && n < N {
				oldChild := oldNode.Children[o]
				newChild := newNode.Children[n]
				if oldChild.Name == newChild.Name {
					diff := oldChild.Diff(newChild)
					added = append(added, diff.Added...)
					deleted = append(deleted, diff.Deleted...)
					modified = append(modified, diff.Modified...)
					o++
					n++
				} else if oldChild.Name < newChild.Name {
					deleted = append(deleted, oldChild.TraverseFiles()...)
					o++
				} else {
					added = append(added, newChild.TraverseFiles()...)
					n++
				}
			}
			for o < O {
				oldChild := oldNode.Children[o]
				deleted = append(deleted, oldChild.TraverseFiles()...)
				o++
			}
			for n < N {
				newChild := newNode.Children[n]
				added = append(added, newChild.TraverseFiles()...)
				n++

			}
		}
	} else {
		added = append(added, newNode.TraverseFiles()...)
		deleted = append(deleted, oldNode.TraverseFiles()...)
	}
	return &FileTreeDiff{Added: added, Deleted: deleted, Modified: modified}
}

func (n *FileTreeNode) ToDataAppConfigJson() (*entity.DataAppConfigJson, error) {
	if n.Name != "dataapp_config.json" || n.NodeType != File {
		return nil, errors.New("invalid dataapp_config.json node")
	}
	result := new(entity.DataAppConfigJson)
	err := json.Unmarshal(n.Content, result)
	if err != nil {
		return nil, fmt.Errorf("unmarshal dataapp_config.json failed: %s", err)
	}
	return result, nil
}

func (n *FileTreeNode) ToHttpEndpoints() ([]*entity.HttpEndpointConfigJson, error) {
	if n.Name != "config.json" || n.NodeType != File {
		return nil, errors.New("invalid config.json node")
	}
	result := make([]*entity.HttpEndpointConfigJson, 0, 10)
	err := json.Unmarshal(n.Content, &result)
	if err != nil {
		return nil, fmt.Errorf("unmarshal config.json failed: %s", err)
	}
	for _, endpoint := range result {
		endpoint.Type = entity.EndpointGithubTypeToInnerType(endpoint.Type)
	}
	return result, nil
}

func (n *FileTreeNode) ToDataSourceClusters() ([]*entity.DataSourceClusterJson, error) {
	if n.Name != "cluster.json" || n.NodeType != File {
		return nil, errors.New("invalid cluster.json node")
	}
	result := make([]*entity.DataSourceClusterJson, 0, 10)
	err := json.Unmarshal(n.Content, &result)
	if err != nil {
		return nil, fmt.Errorf("unmarshal cluster.json failed: %s", err)
	}
	return result, nil
}

func Snapshot2FileTreeNode(snapshot *entity.DataAppSnapshot) (*FileTreeNode, error) {
	root := NewDirectoryNode("root")
	if snapshot == nil {
		return root, nil
	}

	// ${ROOT}/dataapp_config.json
	content, err := snapshot.DataAppConfigContent()
	if err != nil {
		return nil, err
	}
	dataappConfigJson := NewFileNode("dataapp_config.json", content)
	root.AppendChild(dataappConfigJson)

	// ${ROOT}/http_endpoints/config.json
	httpEndpointDir := NewDirectoryNode("http_endpoints")
	root.AppendChild(httpEndpointDir)
	content, err = snapshot.HttpEndpointConfigContent()
	if err != nil {
		return nil, err
	}
	configJson := NewFileNode("config.json", content)
	httpEndpointDir.AppendChild(configJson)

	// ${ROOT}/http_endpoints/sql/
	sqlDir := NewDirectoryNode("sql")
	httpEndpointDir.AppendChild(sqlDir)
	for _, sf := range snapshot.SqlFiles() {
		sqlFile := NewFileNode(sf.FileName, sf.Content)
		sqlDir.AppendChild(sqlFile)
	}

	// ${ROOT}/data_sources/cluster.json
	datasourceDir := NewDirectoryNode("data_sources")
	root.AppendChild(datasourceDir)
	content, err = snapshot.DataSourceClusterContent()
	if err != nil {
		return nil, err
	}
	clusterJson := NewFileNode("cluster.json", content)
	datasourceDir.AppendChild(clusterJson)

	return root, nil
}

func FileTreeNode2Snapshot(root *FileTreeNode) (*entity.DataAppSnapshot, error) {
	dataappConfigJson, httpEndpointDir, datasourceDir, err := extractRootDir(root)
	if err != nil {
		return nil, err
	}
	configJson, sqlDir, err := extractHttpEndpointDir(httpEndpointDir)
	if err != nil {
		return nil, err
	}
	clusterJson, err := extractDatasouceDir(datasourceDir)
	if err != nil {
		return nil, err
	}
	sqlFilesMap := extractSqlDir(sqlDir)

	dataapp, err := dataappConfigJson.ToDataAppConfigJson()
	if err != nil {
		return nil, err
	}
	endpoints, err := configJson.ToHttpEndpoints()
	if err != nil {
		return nil, err
	}
	clusters, err := clusterJson.ToDataSourceClusters()
	if err != nil {
		return nil, err
	}

	snapshotApp := entity.DataApp{
		ExternalAppID: dataapp.AppId,
		AppName:       dataapp.AppName,
		AppType:       entity.DataAppType(dataapp.AppType),
		AppVersion:    dataapp.AppVersion,
		Description:   dataapp.Description,
	}
	snapshotClusters := make([]*entity.AppCluster, 0, 10)
	for _, cluster := range clusters {
		snapshotClusters = append(
			snapshotClusters,
			&entity.AppCluster{ClusterID: cluster.ClusterId},
		)
	}
	snapshotEndpoints := make([]*entity.Endpoint, 0, 10)
	for _, endpoint := range endpoints {
		if endpoint.ReturnType != "json" {
			return nil, errors.New("return_type must be json")
		}
		args, err := json.Marshal(endpoint.Params)
		if err != nil {
			return nil, err
		}
		settings, err := json.Marshal(endpoint.Settings)
		if err != nil {
			return nil, err
		}

		var (
			sqlTemplate string
			clusterID   uint64
			batch       bool
			description string
		)
		if !entity.IsSystemDataEndpoint(endpoint.Type) {
			var ok bool
			sqlTemplate, ok = sqlFilesMap[endpoint.SqlFile]
			if !ok {
				return nil, fmt.Errorf("cloud not find sqlfile: %s", endpoint.SqlFile)
			}
		}
		if endpoint.DataSource != nil {
			clusterID = endpoint.DataSource.ClusterId
		}
		description = url.PathEscape(endpoint.Description)
		batch = endpoint.Batch == 1
		snapshotEndpoints = append(
			snapshotEndpoints,
			&entity.Endpoint{
				Name:        endpoint.Name,
				Description: description,
				Method:      endpoint.Method,
				Endpoint:    endpoint.Endpoint,
				ClusterId:   clusterID,
				Args:        string(args),
				Settings:    string(settings),
				Tag:         endpoint.Tag,
				Batch:       batch,
				SqlTemplate: sqlTemplate,
				Type:        endpoint.Type,
			},
		)
	}
	return &entity.DataAppSnapshot{
		DataApp:   &snapshotApp,
		Endpoints: snapshotEndpoints,
		Clusters:  snapshotClusters,
	}, nil
}

func extractRootDir(root *FileTreeNode) (dataappConfigJson, httpEndpointDir, datasourceDir *FileTreeNode, err error) {
	for _, node := range root.Children {
		if node.Name == "dataapp_config.json" && node.NodeType == File {
			dataappConfigJson = node
		} else if node.Name == "http_endpoints" && node.NodeType == Directory {
			httpEndpointDir = node
		} else if node.Name == "data_sources" && node.NodeType == Directory {
			datasourceDir = node
		}
	}
	if dataappConfigJson == nil {
		return nil, nil, nil, errors.New("missing file dataapp_config.json")
	} else if httpEndpointDir == nil {
		return nil, nil, nil, errors.New("missing directory http_endpoints")
	} else if datasourceDir == nil {
		return nil, nil, nil, errors.New("missing directory data_sources")
	}
	return
}

func extractHttpEndpointDir(httpEndpointDir *FileTreeNode) (configJson, sqlDir *FileTreeNode, err error) {
	for _, node := range httpEndpointDir.Children {
		if node.Name == "config.json" && node.NodeType == File {
			configJson = node
		} else if node.Name == "sql" && node.NodeType == Directory {
			sqlDir = node
		}
	}
	if configJson == nil {
		return nil, nil, errors.New("missing file config.json")
	} else if sqlDir == nil {
		return nil, nil, errors.New("missing directory sql")
	}
	return
}

func extractDatasouceDir(datasourceDir *FileTreeNode) (clusterJson *FileTreeNode, err error) {
	for _, node := range datasourceDir.Children {
		if node.Name == "cluster.json" && node.NodeType == File {
			clusterJson = node
		}
	}
	if clusterJson == nil {
		return nil, errors.New("missing file cluster.json")
	}
	return
}

// traverse sql dir, return filename map to its content
func extractSqlDir(sqlDir *FileTreeNode) map[string]string {
	result := make(map[string]string)
	for _, node := range sqlDir.Children {
		result[fmt.Sprintf("sql/%s", node.Name)] = string(node.Content)
	}
	return result
}
