package cache

import (
	"context"
	"crypto/rand"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	_ "modernc.org/sqlite"
)

type SQLiteStore struct {
	db               *sql.DB
	useFTS           bool
	forceNoFTS       bool
	cachePath        string
	lockPath         string
	cacheRef         string
	ephemeralLock    bool
	memoryAnchorDB   *sql.DB
	memoryAnchorConn *sql.Conn
	closeOnce        sync.Once
	closeErr         error
}

type LockHandle struct {
	file  *os.File
	path  string
	owner WriterOwner
}

type WriterOwner struct {
	Operation string    `json:"operation"`
	RepoID    string    `json:"repo_id,omitempty"`
	StartedAt time.Time `json:"started_at"`
	PID       int       `json:"pid"`
	CacheRef  string    `json:"cache_ref,omitempty"`
	CachePath string    `json:"cache_path,omitempty"` // Legacy read compatibility; new owners do not write paths.
}

type WriterLease struct {
	lock  *LockHandle
	Owner WriterOwner
}

type WriterRequest struct {
	Operation string
	RepoID    string
	LockPath  string
}

func NewSQLiteStore(ctx context.Context, dataSourceName string) (*SQLiteStore, error) {
	return newSQLiteStore(ctx, dataSourceName, false)
}

func newSQLiteStore(ctx context.Context, dataSourceName string, forceNoFTS bool) (*SQLiteStore, error) {
	cachePath, err := cachePathForDataSource(dataSourceName)
	if err != nil {
		return nil, err
	}
	// Open the same physical path used by the lease, rather than resolving an
	// alias again after authority selection. URI/memory forms remain unchanged.
	poolDataSource := cachePath
	var memoryIdentity [16]byte
	if dataSourceName == ":memory:" {
		if _, err := rand.Read(memoryIdentity[:]); err != nil {
			return nil, err
		}
		// A cancelled transaction can discard its worker connection. Retain a
		// private named database through a separate anchor, and initialize these
		// pragmas on every replacement worker, not just the first connection.
		poolDataSource = fmt.Sprintf("file:gitcode-mcp-memory-%x?mode=memory&cache=shared&_pragma=foreign_keys(1)&_pragma=busy_timeout(5000)", memoryIdentity)
	}
	db, err := sql.Open("sqlite", poolDataSource)
	if err != nil {
		return nil, err
	}
	db.SetMaxOpenConns(8)
	db.SetMaxIdleConns(4)
	if dataSourceName == ":memory:" {
		// Serialize worker access as before; the anchor is never used for work.
		db.SetMaxOpenConns(1)
		db.SetMaxIdleConns(1)
	}
	lockPath := writerLockPath(cachePath)
	store := &SQLiteStore{db: db, forceNoFTS: forceNoFTS, cachePath: cachePath, lockPath: lockPath}
	initialized := false
	defer func() {
		if !initialized {
			_ = store.Close()
		}
	}()
	if dataSourceName == ":memory:" {
		// Independent in-memory stores do not share state and therefore must not
		// contend on the process-global fallback lock used for legacy callers.
		store.lockPath = filepath.Join(os.TempDir(), fmt.Sprintf("gitcode-mcp-memory-writer-%x.lock", memoryIdentity))
		store.ephemeralLock = true
		store.memoryAnchorDB, err = sql.Open("sqlite", poolDataSource)
		if err != nil {
			return nil, err
		}
		store.memoryAnchorConn, err = store.memoryAnchorDB.Conn(ctx)
		if err != nil {
			return nil, err
		}
	}
	if _, err := db.ExecContext(ctx, "PRAGMA foreign_keys = ON"); err != nil {
		return nil, err
	}
	if _, err := db.ExecContext(ctx, "PRAGMA busy_timeout = 5000"); err != nil {
		return nil, err
	}
	if dataSourceName != ":memory:" {
		if _, err := db.ExecContext(ctx, "PRAGMA journal_mode = WAL"); err != nil {
			return nil, err
		}
	}
	useFTS := !forceNoFTS && detectFTS5(ctx, db)
	store.useFTS = useFTS
	if dataSourceName == ":memory:" {
		if err := runMigrations(ctx, db, useFTS); err != nil {
			return nil, err
		}
		store.loadCacheRef(ctx)
		initialized = true
		return store, nil
	}
	compat, err := CheckVersionCompatibility(ctx, db)
	if err != nil {
		return nil, err
	}
	if !compat.PermitWrites {
		return nil, &SchemaVersionError{Compat: compat}
	}
	if compat.DetectedVersion == compat.ExpectedVersion {
		store.loadCacheRef(ctx)
		initialized = true
		return store, nil
	}
	lease, err := store.AcquireWriter(ctx, WriterRequest{Operation: "migration"})
	if err != nil {
		return nil, err
	}
	if err := runMigrations(ctx, db, useFTS); err != nil {
		_ = store.ReleaseWriter(context.Background(), lease)
		return nil, err
	}
	if err := store.ReleaseWriter(context.Background(), lease); err != nil {
		return nil, err
	}
	store.loadCacheRef(ctx)
	initialized = true
	return store, nil
}

func NewSQLiteReadOnlyStore(ctx context.Context, dataSourceName string) (*SQLiteStore, error) {
	if _, err := os.Stat(dataSourceName); err != nil {
		return nil, fmt.Errorf("cache: cannot open read-only store: %w", err)
	}
	dsn, err := sqliteReadOnlyDSN(dataSourceName)
	if err != nil {
		return nil, err
	}
	db, err := sql.Open("sqlite", dsn)
	if err != nil {
		return nil, err
	}
	db.SetMaxOpenConns(1)
	db.SetMaxIdleConns(1)
	if _, err := db.ExecContext(ctx, "PRAGMA foreign_keys = ON"); err != nil {
		_ = db.Close()
		return nil, err
	}
	if _, err := db.ExecContext(ctx, "PRAGMA busy_timeout = 5000"); err != nil {
		_ = db.Close()
		return nil, err
	}
	compat, err := CheckVersionCompatibility(ctx, db)
	if err != nil {
		_ = db.Close()
		return nil, err
	}
	if !compat.Compatible {
		_ = db.Close()
		return nil, &SchemaVersionError{Compat: compat}
	}
	useFTS := detectFTS5(ctx, db)
	return &SQLiteStore{db: db, useFTS: useFTS, forceNoFTS: false, cachePath: dataSourceName, lockPath: ""}, nil
}

func sqliteReadOnlyDSN(dataSourceName string) (string, error) {
	return sqliteFileDSN(dataSourceName, false)
}

func sqliteImmutableReadOnlyDSN(dataSourceName string) (string, error) {
	return sqliteFileDSN(dataSourceName, true)
}

func sqliteFileDSN(dataSourceName string, immutable bool) (string, error) {
	absolutePath, err := filepath.Abs(dataSourceName)
	if err != nil {
		return "", fmt.Errorf("cache: resolve read-only store path: %w", err)
	}
	uriPath := filepath.ToSlash(absolutePath)
	if !strings.HasPrefix(uriPath, "/") {
		uriPath = "/" + uriPath
	}
	uri := url.URL{Scheme: "file", Path: uriPath}
	query := uri.Query()
	query.Set("mode", "ro")
	if immutable {
		query.Set("immutable", "1")
	}
	uri.RawQuery = query.Encode()
	return uri.String(), nil
}

func (s *SQLiteStore) SchemaVersion(ctx context.Context) (int, error) {
	return schemaVersion(ctx, s.db)
}

func NewInMemorySQLiteStore(ctx context.Context) (*SQLiteStore, error) {
	return NewSQLiteStore(ctx, ":memory:")
}

func cachePathForDataSource(dataSourceName string) (string, error) {
	if dataSourceName == ":memory:" || strings.HasPrefix(dataSourceName, "file:") {
		return dataSourceName, nil
	}
	abs, err := filepath.Abs(dataSourceName)
	if err != nil {
		return "", writerAuthorityResolutionError(err)
	}
	for depth := 0; depth < 32; depth++ {
		physical, err := filepath.EvalSymlinks(abs)
		if err == nil {
			return physical, nil
		}
		if !os.IsNotExist(err) {
			return "", writerAuthorityResolutionError(err)
		}
		// A dangling final symlink can still be used by SQLite to create its
		// target. Follow it before selecting the not-yet-created file's lease.
		info, statErr := os.Lstat(abs)
		if statErr == nil && info.Mode()&os.ModeSymlink != 0 {
			target, err := os.Readlink(abs)
			if err != nil {
				return "", writerAuthorityResolutionError(err)
			}
			if !filepath.IsAbs(target) {
				target = filepath.Join(filepath.Dir(abs), target)
			}
			abs = filepath.Clean(target)
			continue
		}
		if statErr != nil && !os.IsNotExist(statErr) {
			return "", writerAuthorityResolutionError(statErr)
		}
		parent, err := filepath.EvalSymlinks(filepath.Dir(abs))
		if err != nil {
			return "", writerAuthorityResolutionError(err)
		}
		return filepath.Join(parent, filepath.Base(abs)), nil
	}
	return "", writerAuthorityResolutionError(nil)
}

func writerLockPath(cachePath string) string {
	if cachePath == "" || cachePath == ":memory:" || strings.HasPrefix(cachePath, "file:") {
		return filepath.Join(os.TempDir(), "gitcode-mcp-cache-writer.lock")
	}
	return cachePath + ".writer.lock"
}

func (s *SQLiteStore) loadCacheRef(ctx context.Context) {
	identity, err := s.CacheIdentity(ctx)
	if err != nil {
		return
	}
	s.cacheRef = strings.TrimSpace(identity.UUID)
}

func (s *SQLiteStore) Close() error {
	s.closeOnce.Do(func() {
		s.closeErr = s.db.Close()
		if s.memoryAnchorConn != nil {
			s.closeErr = errors.Join(s.closeErr, s.memoryAnchorConn.Close())
		}
		if s.memoryAnchorDB != nil {
			s.closeErr = errors.Join(s.closeErr, s.memoryAnchorDB.Close())
		}
		if s.ephemeralLock {
			_ = os.Remove(s.lockPath)
		}
	})
	return s.closeErr
}

func marshalJSON(v any) (string, error) {
	b, err := json.Marshal(v)
	if err != nil {
		return "", err
	}
	return string(b), nil
}

func unmarshalJSON[T any](raw string) (T, error) {
	var v T
	if raw == "" {
		return v, nil
	}
	if err := json.Unmarshal([]byte(raw), &v); err != nil {
		return v, err
	}
	return v, nil
}

func execTx(ctx context.Context, tx *sql.Tx, query string, args ...any) error {
	_, err := tx.ExecContext(ctx, query, args...)
	return err
}

func txRollbackOnError(tx *sql.Tx, errp *error) {
	if *errp != nil {
		_ = tx.Rollback()
	}
}

func (s *SQLiteStore) IntegrityCheck(ctx context.Context) error {
	var result string
	if err := s.db.QueryRowContext(ctx, "PRAGMA integrity_check").Scan(&result); err != nil {
		return err
	}
	if result != "ok" {
		return ErrCacheCorruption{Path: "sqlite", Detail: result}
	}
	return nil
}

func (s *SQLiteStore) Checkpoint(ctx context.Context, reason string) error {
	_ = reason
	var busy, logFrames, checkpointed int
	if err := s.db.QueryRowContext(ctx, "PRAGMA wal_checkpoint(PASSIVE)").Scan(&busy, &logFrames, &checkpointed); err != nil {
		return err
	}
	if busy != 0 {
		return ErrLockContention{Path: s.cachePath, HolderHint: "sqlite checkpoint busy"}
	}
	return nil
}

func notFoundErr(thing, id string) error {
	return fmt.Errorf("%w: %s %s", ErrNotFound, thing, id)
}