/*
 * Copyright 2021-2024 JetBrains s.r.o.
 *
 * 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
 *
 * https://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 commoncontext

import (
	"bytes"
	"io"
	"os"
	"path/filepath"
	"slices"
	"sort"
	"strings"

	"github.com/JetBrains/qodana-cli/internal/platform/strutil"
	"github.com/go-enry/go-enry/v2"
	log "github.com/sirupsen/logrus"
)

const (
	QodanaSarifName = "qodana.sarif.json"
)

// ignoredDirectories is a list of directories that should be ignored by the configurator.
var ignoredDirectories = []string{
	".idea",
	".vscode",
	".git",
}

// isInIgnoredDirectory returns true if the given path should be ignored by the configurator.
func isInIgnoredDirectory(path string) bool {
	parts := strings.Split(path, string(os.PathSeparator))
	for _, part := range parts {
		for _, ignored := range ignoredDirectories {
			if part == ignored {
				return true
			}
		}
	}
	return false
}

// recognizeDirLanguages returns the languages detected in the given directory.
func recognizeDirLanguages(projectPath string) ([]string, error) {
	const limitKb = 64
	out := make(map[string]int)
	err := filepath.Walk(
		projectPath, func(path string, f os.FileInfo, err error) error {
			if err != nil {
				return filepath.SkipDir
			}

			if f.Mode().IsDir() && !f.Mode().IsRegular() {
				return nil
			}

			relpath, err := filepath.Rel(projectPath, path)
			relpath = filepath.ToSlash(relpath) // enry always uses forward slashes for regex matching
			if err != nil {
				return nil
			}

			if relpath == "." {
				return nil
			}

			if f.IsDir() {
				relpath += "/"
			}
			if isInIgnoredDirectory(path) || enry.IsVendor(relpath) || enry.IsDotFile(relpath) ||
				enry.IsDocumentation(relpath) || enry.IsConfiguration(relpath) ||
				enry.IsGenerated(relpath, nil) {
				if f.IsDir() {
					return filepath.SkipDir
				}
				return nil
			}

			if f.IsDir() {
				return nil
			}

			content, err := readFile(path, limitKb)
			if err != nil {
				return nil
			}

			if enry.IsGenerated(relpath, content) {
				return nil
			}

			language := enry.GetLanguage(filepath.Base(path), content)
			if language == enry.OtherLanguage {
				return nil
			}

			if enry.GetLanguageType(language) != enry.Programming {
				return nil
			}

			out[language] += 1
			return nil
		},
	)
	if err != nil {
		return nil, err
	}
	type languageCount struct {
		Language string
		Count    int
	}
	langCounts := make([]languageCount, 0, len(out))
	for language, count := range out {
		langCounts = append(langCounts, languageCount{Language: language, Count: count})
	}
	sort.Slice(
		langCounts, func(i, j int) bool {
			return langCounts[i].Count > langCounts[j].Count
		},
	)
	languages := make([]string, 0, len(langCounts))
	for _, langCount := range langCounts {
		languages = append(languages, langCount.Language)
	}

	slices.Sort(languages)
	return languages, nil
}

// readFile reads the file at the given path and returns its content.
func readFile(path string, limit int64) ([]byte, error) {
	if limit <= 0 {
		return os.ReadFile(path)
	}
	f, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer func() {
		err := f.Close()
		if err != nil {
			log.Print(err)
		}
	}()
	st, err := f.Stat()
	if err != nil {
		return nil, err
	}
	size := st.Size()
	if size > limit {
		size = limit
	}
	buf := bytes.NewBuffer(nil)
	buf.Grow(int(size))
	_, err = io.Copy(buf, io.LimitReader(f, limit))
	return buf.Bytes(), err
}

// readIdeaDir reads .idea directory and tries to detect which languages are used in the project.
func readIdeaDir(project string) []string {
	var languages []string
	var files []string
	root := filepath.Join(project, ".idea")
	if _, err := os.Stat(root); os.IsNotExist(err) {
		return languages
	}
	err := filepath.Walk(
		root, func(path string, info os.FileInfo, err error) error {
			files = append(files, path)
			return nil
		},
	)
	if err != nil {
		panic(err)
	}
	for _, file := range files {
		if filepath.Ext(file) == ".iml" {
			iml, err := os.ReadFile(file)
			if err != nil {
				log.Fatal(err)
			}
			text := string(iml)
			if strings.Contains(text, "JAVA_MODULE") {
				languages = strutil.Append(languages, "Java")
			}
			if strings.Contains(text, "PYTHON_MODULE") {
				languages = strutil.Append(languages, "Python")
			}
			if strings.Contains(text, "Go") {
				languages = strutil.Append(languages, "Go")
			}
		}
	}
	return languages
}

// isAndroidProject checks if the given directory is an Android project by checking AndroidManifest
// https://developer.android.com/guide/topics/manifest/manifest-intro
func isAndroidProject(projectDir string) bool {
	var foundManifest bool
	err := filepath.Walk(
		projectDir, func(path string, info os.FileInfo, err error) error {
			if err != nil {
				return err
			}
			if info.IsDir() {
				return nil
			}
			if strings.HasSuffix(info.Name(), "AndroidManifest.xml") {
				foundManifest = true
				return filepath.SkipDir
			}

			return nil
		},
	)
	if err != nil {
		log.Fatal("Error walking the path: ", err)
	}
	return foundManifest
}
