package scanner

import (
	"encoding/json"
	"os"
	"path/filepath"
	"sort"
	"strings"
)

// ApplyNarrative fills project-scanner narrative fields deterministically.
// Manifest/README-derived values satisfy the scan-result contract while rich
// LLM narrative remains a later graph-quality enhancement.
func ApplyNarrative(root string, result *Result) {
	name, rawDescription, manifestFrameworks := manifestNarrative(root)
	readmeContent := readReadmeContent(root)
	readmeHead := firstLines(readmeContent, 10)
	if strings.TrimSpace(name) == "" {
		name = filepath.Base(filepath.Clean(root))
	}
	if strings.TrimSpace(name) == "" || name == "." || name == string(filepath.Separator) {
		name = "project"
	}
	result.Name = name
	result.RawDescription = rawDescription
	result.ReadmeHead = readmeHead
	result.ReadmeContent = readmeContent
	result.Description = synthesizeDescription(name, rawDescription, readmeHead)
	result.Frameworks = normalizeSortedStrings(append(result.Frameworks, manifestFrameworks...))
}

func manifestNarrative(root string) (name string, rawDescription string, frameworks []string) {
	packageJSON := filepath.Join(root, "package.json")
	if data, err := os.ReadFile(packageJSON); err == nil {
		var manifest struct {
			Name                 string            `json:"name"`
			Description          string            `json:"description"`
			Dependencies         map[string]string `json:"dependencies"`
			DevDependencies      map[string]string `json:"devDependencies"`
			OptionalDependencies map[string]string `json:"optionalDependencies"`
		}
		if json.Unmarshal(data, &manifest) == nil {
			name = strings.TrimSpace(manifest.Name)
			rawDescription = strings.TrimSpace(manifest.Description)
			deps := map[string]string{}
			for key, value := range manifest.Dependencies {
				deps[key] = value
			}
			for key, value := range manifest.DevDependencies {
				deps[key] = value
			}
			for key, value := range manifest.OptionalDependencies {
				deps[key] = value
			}
			jsFrameworks := map[string]string{
				"react":         "react",
				"vue":           "vue",
				"svelte":        "svelte",
				"@angular/core": "angular",
				"express":       "express",
				"fastify":       "fastify",
				"koa":           "koa",
				"next":          "nextjs",
				"nuxt":          "nuxt",
				"vite":          "vite",
				"vitest":        "vitest",
				"jest":          "jest",
				"mocha":         "mocha",
				"tailwindcss":   "tailwindcss",
				"prisma":        "prisma",
				"typeorm":       "typeorm",
				"sequelize":     "sequelize",
				"mongoose":      "mongoose",
				"redux":         "redux",
				"zustand":       "zustand",
				"mobx":          "mobx",
			}
			for dependency, framework := range jsFrameworks {
				if _, ok := deps[dependency]; ok {
					frameworks = append(frameworks, framework)
				}
			}
		}
	}
	goMod := filepath.Join(root, "go.mod")
	if data, err := os.ReadFile(goMod); err == nil {
		text := string(data)
		if strings.TrimSpace(name) == "" {
			for _, line := range strings.Split(text, "\n") {
				line = strings.TrimSpace(line)
				if strings.HasPrefix(line, "module ") {
					module := strings.TrimSpace(strings.TrimPrefix(line, "module "))
					if module != "" {
						name = filepath.Base(module)
					}
					break
				}
			}
		}
		goFrameworks := map[string]string{
			"github.com/gin-gonic/gin": "gin",
			"github.com/labstack/echo": "echo",
			"github.com/gofiber/fiber": "fiber",
			"github.com/go-chi/chi":    "chi",
			"gorm.io/gorm":             "gorm",
		}
		for needle, framework := range goFrameworks {
			if strings.Contains(text, needle) {
				frameworks = append(frameworks, framework)
			}
		}
	}
	frameworks = inferInfrastructureFrameworks(root, frameworks)
	sort.Strings(frameworks)
	return name, rawDescription, frameworks
}

func inferInfrastructureFrameworks(root string, frameworks []string) []string {
	if fileExists(filepath.Join(root, "Dockerfile")) {
		frameworks = append(frameworks, "docker")
	}
	if fileExists(filepath.Join(root, "docker-compose.yml")) || fileExists(filepath.Join(root, "docker-compose.yaml")) || fileExists(filepath.Join(root, "compose.yml")) || fileExists(filepath.Join(root, "compose.yaml")) {
		frameworks = append(frameworks, "docker-compose")
	}
	if matches, _ := filepath.Glob(filepath.Join(root, "*.tf")); len(matches) > 0 {
		frameworks = append(frameworks, "terraform")
	}
	if matches, _ := filepath.Glob(filepath.Join(root, ".github", "workflows", "*.yml")); len(matches) > 0 {
		frameworks = append(frameworks, "github-actions")
	}
	if matches, _ := filepath.Glob(filepath.Join(root, ".github", "workflows", "*.yaml")); len(matches) > 0 {
		frameworks = append(frameworks, "github-actions")
	}
	if fileExists(filepath.Join(root, ".gitlab-ci.yml")) {
		frameworks = append(frameworks, "gitlab-ci")
	}
	if fileExists(filepath.Join(root, "Jenkinsfile")) {
		frameworks = append(frameworks, "jenkins")
	}
	return frameworks
}

func readReadmeContent(root string) string {
	for _, name := range []string{"README.md", "README.mdx", "README.rst", "README.txt", "README"} {
		data, err := os.ReadFile(filepath.Join(root, name))
		if err != nil {
			continue
		}
		runes := []rune(string(data))
		if len(runes) > 3000 {
			runes = runes[:3000]
		}
		return string(runes)
	}
	return ""
}

func firstLines(value string, limit int) string {
	lines := strings.Split(value, "\n")
	if len(lines) > limit {
		lines = lines[:limit]
	}
	return strings.TrimSpace(strings.Join(lines, "\n"))
}

func synthesizeDescription(name string, rawDescription string, readmeHead string) string {
	if desc := strings.TrimSpace(rawDescription); desc != "" {
		return desc
	}
	for _, line := range strings.Split(readmeHead, "\n") {
		line = strings.TrimSpace(strings.TrimLeft(line, "# "))
		if isUsefulReadmeDescription(line, name) {
			return line
		}
	}
	return "Codebase graph for " + name + "."
}

func isUsefulReadmeDescription(line string, name string) bool {
	if line == "" || strings.EqualFold(line, name) {
		return false
	}
	lower := strings.ToLower(line)
	if strings.HasPrefix(lower, "[![") || strings.HasPrefix(lower, "![") || strings.HasPrefix(lower, "<img") || strings.HasPrefix(lower, "<p") {
		return false
	}
	return !strings.Contains(lower, "shields.io/") && !strings.Contains(lower, "badge.svg")
}

func fileExists(path string) bool {
	info, err := os.Stat(path)
	return err == nil && info.Mode().IsRegular()
}
