package main

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
)

type sourceMountKind uint8

const (
	sourceMountCollection sourceMountKind = iota
	sourceMountExact
)

type sourceMount struct {
	Kind           sourceMountKind
	Prefix         string
	Directory      string
	SemanticBase   string
	PreservePrefix bool
}

type projectManifest struct {
	Path   string
	Root   string
	Source string
	Mounts []sourceMount
}

var manifestDirective = regexp.MustCompile(`^\s*(sources|source)\s+([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)\s+("(?:\\.|[^"\\])*")\s*(?://.*)?$`)

func findProjectManifest(start string) (*projectManifest, error) {
	dir, err := filepath.Abs(start)
	if err != nil {
		return nil, err
	}
	for {
		path := filepath.Join(dir, "qk.mod")
		data, readErr := os.ReadFile(path)
		if readErr == nil {
			return parseProjectManifest(path, string(data))
		}
		if !os.IsNotExist(readErr) {
			return nil, readErr
		}
		parent := filepath.Dir(dir)
		if parent == dir {
			return nil, nil
		}
		dir = parent
	}
}

func parseProjectManifest(path, source string) (*projectManifest, error) {
	manifest := &projectManifest{Path: path, Root: filepath.Dir(path), Source: source}
	exact := make(map[string]string)
	scanner := bufio.NewScanner(strings.NewReader(source))
	for line := 1; scanner.Scan(); line++ {
		text := strings.TrimSpace(scanner.Text())
		if text == "" || strings.HasPrefix(text, "//") {
			continue
		}
		matches := manifestDirective.FindStringSubmatch(scanner.Text())
		if matches == nil {
			return nil, fmt.Errorf("%s:%d: invalid qk.mod directive", path, line)
		}
		prefix := matches[2]
		if prefix == "std" || strings.HasPrefix(prefix, "std.") || prefix == "main" {
			return nil, fmt.Errorf("%s:%d: source prefix %q is reserved", path, line, prefix)
		}
		relative, err := strconv.Unquote(matches[3])
		if err != nil || relative == "" {
			return nil, fmt.Errorf("%s:%d: source path must be a non-empty quoted string", path, line)
		}
		directory := relative
		if !filepath.IsAbs(directory) {
			directory = filepath.Join(manifest.Root, directory)
		}
		directory, err = filepath.Abs(directory)
		if err != nil {
			return nil, fmt.Errorf("%s:%d: resolve source path: %w", path, line, err)
		}
		info, err := os.Stat(directory)
		if err != nil || !info.IsDir() {
			return nil, fmt.Errorf("%s:%d: source path is not a directory: %s", path, line, directory)
		}
		mount := sourceMount{Prefix: prefix, Directory: directory}
		if matches[1] == "source" {
			if previous := exact[prefix]; previous != "" && pathKey(previous) != pathKey(directory) {
				return nil, fmt.Errorf("%s:%d: conflicting source mount %q maps to both %s and %s", path, line, prefix, previous, directory)
			}
			exact[prefix] = directory
			mount.Kind = sourceMountExact
			parts := strings.Split(prefix, ".")
			mount.SemanticBase = parts[len(parts)-1]
		}
		manifest.Mounts = append(manifest.Mounts, mount)
	}
	if err := scanner.Err(); err != nil {
		return nil, err
	}
	return manifest, nil
}