package persistence

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/kits/agentteam-insight/internal/graphbuilder/schema"
)

type Store struct {
	layout Layout
	fs     fileSystem
	now    func() time.Time
}

type Artifacts struct {
	Graph        schema.KnowledgeGraph
	Fingerprints schema.FingerprintStore
	Meta         schema.AnalysisMeta
	Config       schema.ProjectConfig
	WriteConfig  bool
}

type fileSystem interface {
	MkdirAll(path string, perm fs.FileMode) error
	WriteJSON(path string, value any) error
	ReadFile(path string) ([]byte, error)
	Rename(oldpath string, newpath string) error
	ReadDir(path string) ([]os.DirEntry, error)
	RemoveAll(path string) error
}

type osFileSystem struct{}

func NewStore(inputRoot string, outputRoot string) (*Store, error) {
	layout, err := ResolveLayout(inputRoot, outputRoot)
	if err != nil {
		return nil, err
	}
	return NewStoreWithLayout(layout), nil
}

func NewStoreWithLayout(layout Layout) *Store {
	return &Store{layout: layout, fs: osFileSystem{}, now: time.Now}
}

func (store *Store) Layout() Layout {
	return store.layout
}

func (store *Store) PrepareRun() error {
	if err := store.fs.MkdirAll(store.layout.GraphRoot, 0o755); err != nil {
		return fmt.Errorf("create graph artifact root: %w", err)
	}
	if err := store.fs.MkdirAll(store.layout.IntermediateRoot, 0o755); err != nil {
		return fmt.Errorf("create intermediate dir: %w", err)
	}
	if err := store.fs.MkdirAll(store.layout.TmpRoot, 0o755); err != nil {
		return fmt.Errorf("create tmp dir: %w", err)
	}
	return store.PurgeStaleTrash(7 * 24 * time.Hour)
}

func (store *Store) SaveConfig(config schema.ProjectConfig) error {
	if err := schema.ValidateProjectConfig(config); err != nil {
		return fmt.Errorf("validate %s: %w", ConfigFileName, err)
	}
	if err := store.fs.MkdirAll(store.layout.GraphRoot, 0o755); err != nil {
		return fmt.Errorf("create graph artifact root: %w", err)
	}
	if err := store.fs.WriteJSON(store.layout.ConfigPath, config); err != nil {
		return fmt.Errorf("write %s: %w", ConfigFileName, err)
	}
	return nil
}

func (store *Store) PurgeStaleTrash(retention time.Duration) error {
	entries, err := store.fs.ReadDir(store.layout.GraphRoot)
	if errors.Is(err, os.ErrNotExist) {
		return nil
	}
	if err != nil {
		return fmt.Errorf("read graph artifact root: %w", err)
	}
	cutoff := store.now().UTC().Add(-retention)
	for _, entry := range entries {
		if !entry.IsDir() || !strings.HasPrefix(entry.Name(), ".trash-") {
			continue
		}
		info, err := entry.Info()
		if err != nil {
			return fmt.Errorf("stat trash dir %s: %w", entry.Name(), err)
		}
		if info.ModTime().After(cutoff) {
			continue
		}
		if err := store.fs.RemoveAll(filepath.Join(store.layout.GraphRoot, entry.Name())); err != nil {
			return fmt.Errorf("purge trash dir %s: %w", entry.Name(), err)
		}
	}
	return nil
}

func (store *Store) SaveArtifacts(artifacts Artifacts) error {
	if err := store.fs.MkdirAll(store.layout.GraphRoot, 0o755); err != nil {
		return fmt.Errorf("create graph artifact root: %w", err)
	}
	sanitizedGraph := SanitizeGraphFilePaths(artifacts.Graph, store.layout.InputRoot)
	result := schema.ValidateGraph(sanitizedGraph)
	if !result.Success || result.Data == nil {
		return fmt.Errorf("validate knowledge graph: %s", firstNonEmpty(result.Fatal, "invalid graph"))
	}
	fingerprints := normalizeFingerprintStore(artifacts.Fingerprints)
	if err := schema.ValidateFingerprintStore(fingerprints); err != nil {
		return fmt.Errorf("validate %s: %w", FingerprintFile, err)
	}
	if err := schema.ValidateAnalysisMeta(artifacts.Meta); err != nil {
		return fmt.Errorf("validate %s: %w", MetaFileName, err)
	}
	if artifacts.WriteConfig {
		if err := schema.ValidateProjectConfig(artifacts.Config); err != nil {
			return fmt.Errorf("validate %s: %w", ConfigFileName, err)
		}
	}
	if err := store.fs.WriteJSON(store.layout.GraphPath, result.Data); err != nil {
		return fmt.Errorf("write %s: %w", GraphFileName, err)
	}
	if err := store.fs.WriteJSON(store.layout.FingerprintsPath, fingerprints); err != nil {
		return fmt.Errorf("write %s: %w", FingerprintFile, err)
	}
	if err := store.fs.WriteJSON(store.layout.MetaPath, artifacts.Meta); err != nil {
		return fmt.Errorf("write %s: %w", MetaFileName, err)
	}
	if artifacts.WriteConfig {
		if err := store.fs.WriteJSON(store.layout.ConfigPath, artifacts.Config); err != nil {
			return fmt.Errorf("write %s: %w", ConfigFileName, err)
		}
	}
	if err := store.CleanupTransientIntermediate(); err != nil {
		return err
	}
	return nil
}

func (store *Store) SaveDiffOverlay(overlay schema.DiffOverlay) error {
	if err := schema.ValidateDiffOverlay(overlay); err != nil {
		return fmt.Errorf("validate %s: %w", DiffOverlayFile, err)
	}
	if err := store.fs.MkdirAll(store.layout.GraphRoot, 0o755); err != nil {
		return fmt.Errorf("create graph artifact root: %w", err)
	}
	if err := store.fs.WriteJSON(store.layout.DiffOverlayPath, overlay); err != nil {
		return fmt.Errorf("write %s: %w", DiffOverlayFile, err)
	}
	return nil
}

func (store *Store) SaveDomainGraph(graph schema.KnowledgeGraph) error {
	sanitized := SanitizeGraphFilePaths(graph, store.layout.InputRoot)
	result := schema.ValidateGraph(sanitized)
	if !result.Success || result.Data == nil {
		return fmt.Errorf("validate %s: %s", DomainGraphFile, firstNonEmpty(result.Fatal, "invalid graph"))
	}
	if err := schema.ValidateDomainGraph(*result.Data); err != nil {
		return fmt.Errorf("validate %s: %w", DomainGraphFile, err)
	}
	if err := store.fs.WriteJSON(store.layout.DomainGraphPath, result.Data); err != nil {
		return fmt.Errorf("write %s: %w", DomainGraphFile, err)
	}
	return nil
}

func (store *Store) LoadGraph(validate bool) (*schema.KnowledgeGraph, error) {
	data, err := store.fs.ReadFile(store.layout.GraphPath)
	if errors.Is(err, os.ErrNotExist) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	if validate {
		result := schema.ValidateGraphJSON(data)
		if !result.Success || result.Data == nil {
			return nil, fmt.Errorf("invalid knowledge graph: %s", firstNonEmpty(result.Fatal, "invalid graph"))
		}
		return result.Data, nil
	}
	var graph schema.KnowledgeGraph
	if err := json.Unmarshal(data, &graph); err != nil {
		return nil, err
	}
	return &graph, nil
}

func (store *Store) LoadMeta() (*schema.AnalysisMeta, error) {
	var meta schema.AnalysisMeta
	if ok, err := store.loadJSONIfExists(store.layout.MetaPath, &meta); !ok || err != nil {
		return nil, err
	}
	return &meta, nil
}

func (store *Store) LoadFingerprints() (*schema.FingerprintStore, error) {
	var fingerprints schema.FingerprintStore
	if ok, err := store.loadJSONIfExists(store.layout.FingerprintsPath, &fingerprints); !ok || err != nil {
		return nil, err
	}
	return &fingerprints, nil
}

func (store *Store) LoadConfig() (schema.ProjectConfig, error) {
	var config schema.ProjectConfig
	ok, err := store.loadJSONIfExists(store.layout.ConfigPath, &config)
	if err != nil {
		return schema.ProjectConfig{AutoUpdate: false, OutputLanguage: "en"}, nil
	}
	if !ok {
		return schema.ProjectConfig{AutoUpdate: false, OutputLanguage: "en"}, nil
	}
	return config, nil
}

func (store *Store) CleanupTransientIntermediate() error {
	entries, err := store.fs.ReadDir(store.layout.IntermediateRoot)
	if err != nil && !errors.Is(err, os.ErrNotExist) {
		return fmt.Errorf("read intermediate dir: %w", err)
	}

	type trashMove struct {
		from string
		name string
	}
	var moves []trashMove
	if err == nil {
		sort.Slice(entries, func(i, j int) bool { return entries[i].Name() < entries[j].Name() })
		for _, entry := range entries {
			if preserveIntermediate(entry.Name()) {
				continue
			}
			moves = append(moves, trashMove{from: filepath.Join(store.layout.IntermediateRoot, entry.Name()), name: entry.Name()})
		}
	}

	if _, err := store.fs.ReadDir(store.layout.TmpRoot); err == nil {
		moves = append(moves, trashMove{from: store.layout.TmpRoot, name: TmpName})
	} else if !errors.Is(err, os.ErrNotExist) {
		return fmt.Errorf("read tmp dir: %w", err)
	}

	if len(moves) == 0 {
		return nil
	}
	trashRoot, err := store.createTrashRoot()
	if err != nil {
		return err
	}
	for _, move := range moves {
		to := filepath.Join(trashRoot, move.name)
		if err := store.fs.Rename(move.from, to); err != nil {
			return fmt.Errorf("move %s to trash: %w", move.name, err)
		}
	}
	return nil
}

func preserveIntermediate(name string) bool {
	switch name {
	case ScanResultFile, ReviewFile:
		return true
	default:
		return false
	}
}

func (store *Store) createTrashRoot() (string, error) {
	base := filepath.Join(store.layout.GraphRoot, ".trash-"+store.now().UTC().Format("20060102T150405Z"))
	for i := 0; i < 100; i++ {
		candidate := base
		if i > 0 {
			candidate = fmt.Sprintf("%s-%d", base, i)
		}
		entries, err := store.fs.ReadDir(candidate)
		if err == nil && len(entries) > 0 {
			continue
		}
		if err != nil && !errors.Is(err, os.ErrNotExist) {
			return "", fmt.Errorf("read trash dir: %w", err)
		}
		if err := store.fs.MkdirAll(candidate, 0o755); err != nil {
			return "", fmt.Errorf("create trash dir: %w", err)
		}
		return candidate, nil
	}
	return "", fmt.Errorf("create trash dir: no available timestamp slot for %s", base)
}

func (store *Store) loadJSONIfExists(path string, target any) (bool, error) {
	data, err := store.fs.ReadFile(path)
	if errors.Is(err, os.ErrNotExist) {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	if err := json.Unmarshal(data, target); err != nil {
		return false, err
	}
	return true, nil
}

func SanitizeGraphFilePaths(graph schema.KnowledgeGraph, inputRoot string) schema.KnowledgeGraph {
	for i := range graph.Nodes {
		graph.Nodes[i].FilePath = SanitizeFilePath(inputRoot, graph.Nodes[i].FilePath)
	}
	return graph
}

func normalizeFingerprintStore(store schema.FingerprintStore) schema.FingerprintStore {
	if store.Files == nil {
		store.Files = map[string]schema.FileFingerprint{}
	}
	for key, file := range store.Files {
		if file.Functions == nil {
			file.Functions = []schema.FunctionFingerprint{}
		}
		if file.Classes == nil {
			file.Classes = []schema.ClassFingerprint{}
		}
		if file.Imports == nil {
			file.Imports = []schema.ImportFingerprint{}
		}
		if file.Exports == nil {
			file.Exports = []string{}
		}
		store.Files[key] = file
	}
	return store
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return strings.TrimSpace(value)
		}
	}
	return ""
}

func (osFileSystem) MkdirAll(path string, perm fs.FileMode) error {
	return os.MkdirAll(path, perm)
}

func (osFileSystem) WriteJSON(path string, value any) error {
	return writeJSONAtomic(path, value)
}

func (osFileSystem) ReadFile(path string) ([]byte, error) {
	return os.ReadFile(path)
}

func (osFileSystem) Rename(oldpath string, newpath string) error {
	return os.Rename(oldpath, newpath)
}

func (osFileSystem) ReadDir(path string) ([]os.DirEntry, error) {
	return os.ReadDir(path)
}

func (osFileSystem) RemoveAll(path string) error {
	return os.RemoveAll(path)
}

func writeJSONAtomic(path string, value any) error {
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return err
	}
	data, err := json.MarshalIndent(value, "", "  ")
	if err != nil {
		return err
	}
	data = append(data, '\n')
	tmp, err := os.CreateTemp(filepath.Dir(path), "."+filepath.Base(path)+"-*.tmp")
	if err != nil {
		return err
	}
	tmpPath := tmp.Name()
	defer func() { _ = os.Remove(tmpPath) }()
	if _, err := tmp.Write(data); err != nil {
		_ = tmp.Close()
		return err
	}
	if err := tmp.Sync(); err != nil {
		_ = tmp.Close()
		return err
	}
	if err := tmp.Close(); err != nil {
		return err
	}
	if err := os.Rename(tmpPath, path); err != nil {
		return err
	}
	return syncDir(filepath.Dir(path))
}

func syncDir(path string) error {
	dir, err := os.Open(path)
	if err != nil {
		return err
	}
	defer func() { _ = dir.Close() }()
	return dir.Sync()
}
