package store

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"sync"

	"bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/contracts/graph"
)

var _ ArtifactStore = (*FilesystemStore)(nil)

// FilesystemStore persists graph artifacts as JSON files on disk.
type FilesystemStore struct {
	mu        sync.Mutex
	storePath string
}

// NewFilesystemStore creates a store at storePath, creating the directory if needed.
func NewFilesystemStore(storePath string) (*FilesystemStore, error) {
	if strings.TrimSpace(storePath) == "" {
		return nil, errors.New("artifact store path is required")
	}

	absPath, err := filepath.Abs(storePath)
	if err != nil {
		return nil, fmt.Errorf("resolve artifact store path: %w", err)
	}

	if err := os.MkdirAll(absPath, 0o755); err != nil {
		return nil, fmt.Errorf("create artifact store directory: %w", err)
	}

	return &FilesystemStore{storePath: absPath}, nil
}

// SaveArtifact writes artifact JSON and returns a deterministic artifact ID and file URI.
func (s *FilesystemStore) SaveArtifact(ctx context.Context, artifact graph.Artifact) (string, string, error) {
	if err := ctx.Err(); err != nil {
		return "", "", err
	}

	if err := validateArtifactMetadata(artifact); err != nil {
		return "", "", err
	}

	filePath, err := filePathFromArtifact(artifact)
	if err != nil {
		return "", "", err
	}

	id := BuildArtifactID(artifact.SnapshotID, artifact.RepoID, artifact.CommitSHA, filePath)

	data, err := json.Marshal(artifact)
	if err != nil {
		return "", "", fmt.Errorf("marshal artifact: %w", err)
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	finalPath := s.artifactPath(id)
	tmpPath := filepath.Join(s.storePath, "."+id+".tmp")

	if err := os.WriteFile(tmpPath, data, 0o644); err != nil {
		return "", "", fmt.Errorf("write artifact temp file: %w", err)
	}

	if err := os.Rename(tmpPath, finalPath); err != nil {
		_ = os.Remove(tmpPath)
		return "", "", fmt.Errorf("rename artifact file: %w", err)
	}

	return id, fileURI(finalPath), nil
}

// GetArtifact loads a graph artifact by ID.
func (s *FilesystemStore) GetArtifact(ctx context.Context, artifactID string) (graph.Artifact, error) {
	var artifact graph.Artifact

	if err := ctx.Err(); err != nil {
		return artifact, err
	}

	if err := validateArtifactID(artifactID); err != nil {
		return artifact, err
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	data, err := os.ReadFile(s.artifactPath(artifactID))
	if err != nil {
		if os.IsNotExist(err) {
			return artifact, fmt.Errorf("%w: %s", ErrArtifactNotFound, artifactID)
		}
		return artifact, fmt.Errorf("read artifact file: %w", err)
	}

	if err := json.Unmarshal(data, &artifact); err != nil {
		return artifact, fmt.Errorf("unmarshal artifact: %w", err)
	}

	return artifact, nil
}

// GetArtifactMetadata loads artifact summary fields without returning full graph nodes/edges.
func (s *FilesystemStore) GetArtifactMetadata(ctx context.Context, artifactID string) (ArtifactMetadata, error) {
	artifact, err := s.GetArtifact(ctx, artifactID)
	if err != nil {
		return ArtifactMetadata{}, err
	}

	filePath, err := filePathFromArtifact(artifact)
	if err != nil {
		return ArtifactMetadata{}, err
	}

	return ArtifactMetadata{
		ArtifactID:    artifactID,
		SchemaVersion: artifact.SchemaVersion,
		RepoID:        artifact.RepoID,
		SnapshotID:    artifact.SnapshotID,
		CommitSHA:     artifact.CommitSHA,
		FilePath:      filePath,
		NodeCount:     len(artifact.Nodes),
		EdgeCount:     len(artifact.Edges),
		ArtifactURI:   fileURI(s.artifactPath(artifactID)),
	}, nil
}

func (s *FilesystemStore) artifactPath(artifactID string) string {
	return filepath.Join(s.storePath, artifactID+".json")
}

func (s *FilesystemStore) deltaPath(deltaID string) string {
	return filepath.Join(s.storePath, deltaID+".json")
}

// FindArtifactByCommitFile scans the store for a graph artifact matching repo, commit, and file path.
func (s *FilesystemStore) FindArtifactByCommitFile(ctx context.Context, repoID, commitSHA, filePath string) (graph.Artifact, string, error) {
	var empty graph.Artifact
	if err := ctx.Err(); err != nil {
		return empty, "", err
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	entries, err := os.ReadDir(s.storePath)
	if err != nil {
		return empty, "", fmt.Errorf("read artifact store: %w", err)
	}

	for _, entry := range entries {
		if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
			continue
		}
		if !strings.HasPrefix(entry.Name(), "art_") {
			continue
		}

		artifactID := strings.TrimSuffix(entry.Name(), ".json")
		data, err := os.ReadFile(filepath.Join(s.storePath, entry.Name()))
		if err != nil {
			continue
		}

		var artifact graph.Artifact
		if err := json.Unmarshal(data, &artifact); err != nil {
			continue
		}
		if artifact.RepoID != repoID || artifact.CommitSHA != commitSHA {
			continue
		}

		artifactFilePath, err := filePathFromArtifact(artifact)
		if err != nil || artifactFilePath != filePath {
			continue
		}

		return artifact, artifactID, nil
	}

	return empty, "", fmt.Errorf("%w: repo=%s commit=%s file=%s", ErrArtifactNotFound, repoID, commitSHA, filePath)
}

// FindArtifactBySnapshotFile scans the store for a graph artifact matching repo, snapshot, and file path.
func (s *FilesystemStore) FindArtifactBySnapshotFile(ctx context.Context, repoID, snapshotID, filePath string) (graph.Artifact, string, error) {
	var empty graph.Artifact
	if err := ctx.Err(); err != nil {
		return empty, "", err
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	entries, err := os.ReadDir(s.storePath)
	if err != nil {
		return empty, "", fmt.Errorf("read artifact store: %w", err)
	}

	for _, entry := range entries {
		if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
			continue
		}
		if !strings.HasPrefix(entry.Name(), "art_") {
			continue
		}

		artifactID := strings.TrimSuffix(entry.Name(), ".json")
		data, err := os.ReadFile(filepath.Join(s.storePath, entry.Name()))
		if err != nil {
			continue
		}

		var artifact graph.Artifact
		if err := json.Unmarshal(data, &artifact); err != nil {
			continue
		}
		if artifact.RepoID != repoID || artifact.SnapshotID != snapshotID {
			continue
		}

		artifactFilePath, err := filePathFromArtifact(artifact)
		if err != nil || artifactFilePath != filePath {
			continue
		}

		return artifact, artifactID, nil
	}

	return empty, "", fmt.Errorf("%w: repo=%s snapshot=%s file=%s", ErrArtifactNotFound, repoID, snapshotID, filePath)
}

// SaveDelta writes a graph delta JSON artifact and returns its ID and URI.
func (s *FilesystemStore) SaveDelta(ctx context.Context, delta graph.Delta) (string, string, error) {
	if err := ctx.Err(); err != nil {
		return "", "", err
	}
	if strings.TrimSpace(delta.RepoID) == "" {
		return "", "", errors.New("delta repo_id is required")
	}
	if strings.TrimSpace(delta.BaseCommitSHA) == "" || strings.TrimSpace(delta.TargetCommitSHA) == "" {
		return "", "", errors.New("delta base and target commit_sha are required")
	}
	if delta.SchemaVersion == "" {
		delta.SchemaVersion = graph.DeltaSchemaVersionV1
	}

	id := BuildDeltaID(delta.RepoID, delta.BaseCommitSHA, delta.TargetCommitSHA)
	data, err := json.Marshal(delta)
	if err != nil {
		return "", "", fmt.Errorf("marshal delta: %w", err)
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	finalPath := s.deltaPath(id)
	tmpPath := filepath.Join(s.storePath, "."+id+".tmp")
	if err := os.WriteFile(tmpPath, data, 0o644); err != nil {
		return "", "", fmt.Errorf("write delta temp file: %w", err)
	}
	if err := os.Rename(tmpPath, finalPath); err != nil {
		_ = os.Remove(tmpPath)
		return "", "", fmt.Errorf("rename delta file: %w", err)
	}

	return id, fileURI(finalPath), nil
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func validateArtifactMetadata(artifact graph.Artifact) error {
	if strings.TrimSpace(artifact.RepoID) == "" {
		return errors.New("artifact repo_id is required")
	}
	if strings.TrimSpace(artifact.SnapshotID) == "" {
		return errors.New("artifact snapshot_id is required")
	}
	if strings.TrimSpace(artifact.CommitSHA) == "" {
		return errors.New("artifact commit_sha is required")
	}
	return nil
}

func filePathFromArtifact(artifact graph.Artifact) (string, error) {
	for _, node := range artifact.Nodes {
		if node.Kind == graph.NodeKindFile && strings.TrimSpace(node.Path) != "" {
			return node.Path, nil
		}
	}
	return "", errors.New("artifact file node with path is required")
}

func fileURI(absPath string) string {
	return "file://" + absPath
}

// Ready reports whether the artifact store directory exists and is writable.
func (s *FilesystemStore) Ready(ctx context.Context) bool {
	if err := ctx.Err(); err != nil {
		return false
	}

	info, err := os.Stat(s.storePath)
	if err != nil || !info.IsDir() {
		return false
	}

	testFile := filepath.Join(s.storePath, ".ready_check")
	file, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644)
	if err != nil {
		return false
	}
	_ = file.Close()
	_ = os.Remove(testFile)
	return true
}

func validateArtifactID(id string) error {
	if strings.TrimSpace(id) == "" {
		return ErrInvalidArtifactID
	}
	if strings.Contains(id, "..") || strings.ContainsAny(id, `/\`) {
		return ErrInvalidArtifactID
	}
	return nil
}
