// Package file implements a JSON-on-disk metadata store used for local
// development and tests.
package file

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

	"github.com/lukaszraczylo/gohoarder/pkg/metadata"
	"github.com/rs/zerolog/log"
)

// Store implements a file-based metadata store
type Store struct {
	basePath string
	mu       sync.RWMutex
}

// Config holds file store configuration
type Config struct {
	Path string
}

// New creates a new file-based metadata store
func New(cfg Config) (*Store, error) {
	if cfg.Path == "" {
		cfg.Path = "./metadata"
	}

	// Create directory if it doesn't exist
	if err := os.MkdirAll(cfg.Path, 0750); err != nil {
		return nil, fmt.Errorf("failed to create metadata directory: %w", err)
	}

	log.Info().
		Str("path", cfg.Path).
		Msg("File-based metadata store initialized")

	return &Store{
		basePath: cfg.Path,
	}, nil
}

// SavePackage saves package metadata
func (s *Store) SavePackage(ctx context.Context, pkg *metadata.Package) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	// Create registry directory
	regDir := filepath.Join(s.basePath, pkg.Registry)
	if err := os.MkdirAll(regDir, 0750); err != nil {
		return err
	}

	// Save to file
	filename := filepath.Join(regDir, fmt.Sprintf("%s-%s.json", pkg.Name, pkg.Version))
	data, err := json.MarshalIndent(pkg, "", "  ")
	if err != nil {
		return err
	}

	return os.WriteFile(filename, data, 0600)
}

// GetPackage retrieves package metadata
func (s *Store) GetPackage(ctx context.Context, registry, name, version string) (*metadata.Package, error) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	filename := filepath.Join(s.basePath, registry, fmt.Sprintf("%s-%s.json", name, version))
	data, err := os.ReadFile(filename) // #nosec G304 -- Filename is from internal registry structure
	if err != nil {
		if os.IsNotExist(err) {
			return nil, nil
		}
		return nil, err
	}

	var pkg metadata.Package
	if err := json.Unmarshal(data, &pkg); err != nil {
		return nil, err
	}

	return &pkg, nil
}

// ListPackages lists all packages
func (s *Store) ListPackages(ctx context.Context, opts *metadata.ListOptions) ([]*metadata.Package, error) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	var packages []*metadata.Package

	// Walk through all files
	err := filepath.Walk(s.basePath, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		if info.IsDir() || filepath.Ext(path) != ".json" {
			return nil
		}

		data, err := os.ReadFile(path) // #nosec G304 -- Path from internal file structure
		if err != nil {
			return nil // Skip files we can't read
		}

		var pkg metadata.Package
		if err := json.Unmarshal(data, &pkg); err != nil {
			return nil // Skip invalid JSON
		}

		packages = append(packages, &pkg)
		return nil
	})

	if err != nil {
		return nil, err
	}

	// Apply pagination if options provided
	if opts != nil {
		if opts.Offset >= len(packages) {
			return []*metadata.Package{}, nil
		}

		end := opts.Offset + opts.Limit
		if end > len(packages) {
			end = len(packages)
		}

		return packages[opts.Offset:end], nil
	}

	return packages, nil
}

// DeletePackage deletes package metadata
func (s *Store) DeletePackage(ctx context.Context, registry, name, version string) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	filename := filepath.Join(s.basePath, registry, fmt.Sprintf("%s-%s.json", name, version))
	if err := os.Remove(filename); err != nil && !os.IsNotExist(err) {
		return err
	}

	return nil
}

// SaveScanResult saves scan result
func (s *Store) SaveScanResult(ctx context.Context, result *metadata.ScanResult) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	// Create scans directory
	scanDir := filepath.Join(s.basePath, "scans", result.Registry, result.PackageName)
	if err := os.MkdirAll(scanDir, 0750); err != nil {
		return err
	}

	// Save to file with timestamp
	timestamp := time.Now().Unix()
	filename := filepath.Join(scanDir, fmt.Sprintf("%s-%d.json", result.PackageVersion, timestamp))
	data, err := json.MarshalIndent(result, "", "  ")
	if err != nil {
		return err
	}

	return os.WriteFile(filename, data, 0600)
}

// UpdateDownloadCount increments download counter
func (s *Store) UpdateDownloadCount(ctx context.Context, registry, name, version string) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	// Load package
	pkg, err := s.GetPackage(ctx, registry, name, version)
	if err != nil || pkg == nil {
		return err
	}

	// Increment counter
	pkg.DownloadCount++
	pkg.LastAccessed = time.Now()

	// Save back
	return s.SavePackage(ctx, pkg)
}

// GetStats returns statistics for a registry
func (s *Store) GetStats(ctx context.Context, registry string) (*metadata.Stats, error) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	stats := &metadata.Stats{
		Registry:    registry,
		LastUpdated: time.Now(),
	}

	// Walk through files and calculate stats
	err := filepath.Walk(s.basePath, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		if info.IsDir() || filepath.Ext(path) != ".json" {
			return nil
		}

		data, err := os.ReadFile(path) // #nosec G304 -- Path from internal file structure
		if err != nil {
			return nil
		}

		var pkg metadata.Package
		if err := json.Unmarshal(data, &pkg); err != nil {
			return nil
		}

		// Filter by registry if specified
		if registry != "" && pkg.Registry != registry {
			return nil
		}

		stats.TotalPackages++
		stats.TotalSize += pkg.Size
		stats.TotalDownloads += pkg.DownloadCount

		if pkg.SecurityScanned {
			stats.ScannedPackages++
		}

		return nil
	})

	if err != nil {
		return nil, err
	}

	return stats, nil
}

// GetScanResult retrieves latest scan result
func (s *Store) GetScanResult(ctx context.Context, registry, name, version string) (*metadata.ScanResult, error) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	scanDir := filepath.Join(s.basePath, "scans", registry, name)
	pattern := filepath.Join(scanDir, fmt.Sprintf("%s-*.json", version))

	matches, err := filepath.Glob(pattern)
	if err != nil {
		return nil, err
	}

	if len(matches) == 0 {
		return nil, nil
	}

	// Get the latest file
	latestFile := matches[len(matches)-1]
	data, err := os.ReadFile(latestFile) // #nosec G304 -- Path from glob match on internal structure
	if err != nil {
		return nil, err
	}

	var result metadata.ScanResult
	if err := json.Unmarshal(data, &result); err != nil {
		return nil, err
	}

	return &result, nil
}

// Count returns total number of packages
func (s *Store) Count(ctx context.Context) (int, error) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	count := 0
	err := filepath.Walk(s.basePath, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		if !info.IsDir() && filepath.Ext(path) == ".json" && filepath.Dir(path) != filepath.Join(s.basePath, "scans") {
			count++
		}

		return nil
	})

	if err != nil {
		return 0, err
	}

	return count, nil
}

// Health checks if the store is healthy
func (s *Store) Health(ctx context.Context) error {
	// Check if directory is accessible
	_, err := os.Stat(s.basePath)
	return err
}

// SaveCVEBypass saves a CVE bypass (admin only)
func (s *Store) SaveCVEBypass(ctx context.Context, bypass *metadata.CVEBypass) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	// Create bypasses directory
	bypassesDir := filepath.Join(s.basePath, "bypasses")
	if err := os.MkdirAll(bypassesDir, 0750); err != nil {
		return err
	}

	// Save to file
	filename := filepath.Join(bypassesDir, fmt.Sprintf("%s.json", bypass.ID))
	data, err := json.MarshalIndent(bypass, "", "  ")
	if err != nil {
		return err
	}

	return os.WriteFile(filename, data, 0600)
}

// GetActiveCVEBypasses retrieves all active (non-expired) CVE bypasses
func (s *Store) GetActiveCVEBypasses(ctx context.Context) ([]*metadata.CVEBypass, error) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	bypassesDir := filepath.Join(s.basePath, "bypasses")
	var bypasses []*metadata.CVEBypass
	now := time.Now()

	// Read all bypass files
	err := filepath.Walk(bypassesDir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			if os.IsNotExist(err) {
				return nil // bypasses directory doesn't exist yet
			}
			return err
		}

		if info.IsDir() || filepath.Ext(path) != ".json" {
			return nil
		}

		data, err := os.ReadFile(path) // #nosec G304 -- Path from internal file structure
		if err != nil {
			return err
		}

		var bypass metadata.CVEBypass
		if err := json.Unmarshal(data, &bypass); err != nil {
			log.Warn().Err(err).Str("file", path).Msg("Failed to unmarshal bypass")
			return nil
		}

		// Only include active and non-expired bypasses
		if bypass.Active && bypass.ExpiresAt.After(now) {
			bypasses = append(bypasses, &bypass)
		}

		return nil
	})

	if err != nil {
		return nil, err
	}

	return bypasses, nil
}

// ListCVEBypasses lists all CVE bypasses (including expired)
func (s *Store) ListCVEBypasses(ctx context.Context, opts *metadata.BypassListOptions) ([]*metadata.CVEBypass, error) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	bypassesDir := filepath.Join(s.basePath, "bypasses")
	var bypasses []*metadata.CVEBypass
	now := time.Now()

	// Read all bypass files
	err := filepath.Walk(bypassesDir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			if os.IsNotExist(err) {
				return nil // bypasses directory doesn't exist yet
			}
			return err
		}

		if info.IsDir() || filepath.Ext(path) != ".json" {
			return nil
		}

		data, err := os.ReadFile(path) // #nosec G304 -- Path from internal file structure
		if err != nil {
			return err
		}

		var bypass metadata.CVEBypass
		if err := json.Unmarshal(data, &bypass); err != nil {
			log.Warn().Err(err).Str("file", path).Msg("Failed to unmarshal bypass")
			return nil
		}

		// Apply filters if options provided
		if opts != nil {
			if opts.Type != "" && bypass.Type != opts.Type {
				return nil
			}

			if !opts.IncludeExpired && bypass.ExpiresAt.Before(now) {
				return nil
			}

			if opts.ActiveOnly && !bypass.Active {
				return nil
			}
		}

		bypasses = append(bypasses, &bypass)

		return nil
	})

	if err != nil {
		return nil, err
	}

	// Apply limit and offset if specified
	if opts != nil {
		if opts.Offset > 0 && opts.Offset < len(bypasses) {
			bypasses = bypasses[opts.Offset:]
		} else if opts.Offset >= len(bypasses) {
			return []*metadata.CVEBypass{}, nil
		}

		if opts.Limit > 0 && opts.Limit < len(bypasses) {
			bypasses = bypasses[:opts.Limit]
		}
	}

	return bypasses, nil
}

// DeleteCVEBypass deletes a CVE bypass by ID
func (s *Store) DeleteCVEBypass(ctx context.Context, id string) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	filename := filepath.Join(s.basePath, "bypasses", fmt.Sprintf("%s.json", id))
	err := os.Remove(filename)
	if err != nil {
		if os.IsNotExist(err) {
			return fmt.Errorf("CVE bypass not found: %s", id)
		}
		return err
	}

	return nil
}

// CleanupExpiredBypasses removes expired bypasses
func (s *Store) CleanupExpiredBypasses(ctx context.Context) (int, error) {
	s.mu.Lock()
	defer s.mu.Unlock()

	bypassesDir := filepath.Join(s.basePath, "bypasses")
	count := 0
	now := time.Now()

	// Read all bypass files
	err := filepath.Walk(bypassesDir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			if os.IsNotExist(err) {
				return nil // bypasses directory doesn't exist yet
			}
			return err
		}

		if info.IsDir() || filepath.Ext(path) != ".json" {
			return nil
		}

		data, err := os.ReadFile(path) // #nosec G304 -- Path from internal file structure
		if err != nil {
			return err
		}

		var bypass metadata.CVEBypass
		if err := json.Unmarshal(data, &bypass); err != nil {
			log.Warn().Err(err).Str("file", path).Msg("Failed to unmarshal bypass")
			return nil
		}

		// Delete if expired
		if bypass.ExpiresAt.Before(now) {
			if err := os.Remove(path); err != nil {
				log.Warn().Err(err).Str("file", path).Msg("Failed to delete expired bypass")
			} else {
				count++
			}
		}

		return nil
	})

	if err != nil {
		return 0, err
	}

	return count, nil
}

// GetTimeSeriesStats returns time-series download statistics
// File-based store doesn't support time-series statistics
func (s *Store) GetTimeSeriesStats(ctx context.Context, period string, registry string) (*metadata.TimeSeriesStats, error) {
	// Return empty time-series data for file-based store
	return &metadata.TimeSeriesStats{
		Period:     period,
		Registry:   registry,
		DataPoints: []*metadata.TimeSeriesDataPoint{},
	}, nil
}

// AggregateDownloadData aggregates download data
// File-based store doesn't support aggregation
func (s *Store) AggregateDownloadData(ctx context.Context) error {
	// No-op for file-based store
	return nil
}

// Close closes the store
func (s *Store) Close() error {
	// Nothing to close for file-based store
	return nil
}

// SaveAPIKey is a stub: file-backed metadata does not persist API keys.
// The auth.Manager treats ErrNotImplemented as "in-memory only" mode.
func (s *Store) SaveAPIKey(ctx context.Context, key *metadata.APIKey) error {
	return metadata.ErrNotImplemented
}

// GetAPIKey is a stub. See SaveAPIKey.
func (s *Store) GetAPIKey(ctx context.Context, id string) (*metadata.APIKey, error) {
	return nil, metadata.ErrNotImplemented
}

// ListAPIKeys is a stub. See SaveAPIKey.
func (s *Store) ListAPIKeys(ctx context.Context) ([]*metadata.APIKey, error) {
	return nil, metadata.ErrNotImplemented
}

// DeleteAPIKey is a stub. See SaveAPIKey.
func (s *Store) DeleteAPIKey(ctx context.Context, id string) error {
	return metadata.ErrNotImplemented
}

// UpdateAPIKeyLastUsed is a stub. See SaveAPIKey.
func (s *Store) UpdateAPIKeyLastUsed(ctx context.Context, id string, t time.Time) error {
	return metadata.ErrNotImplemented
}
