package remote

import (
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"

	stdgrpc "google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"

	agateGrpc "gitea.unprism.ru/KRBL/Agate/grpc"
	"gitea.unprism.ru/KRBL/Agate/interfaces"
	"gitea.unprism.ru/KRBL/Agate/store"
)

// Client represents a client for connecting to a remote snapshot server
// It implements the interfaces.SnapshotClient interface
type Client struct {
	conn   *stdgrpc.ClientConn
	client agateGrpc.SnapshotServiceClient
}

// Ensure Client implements interfaces.SnapshotClient
var _ interfaces.SnapshotClient = (*Client)(nil)

// NewClient creates a new client connected to the specified address
func NewClient(address string) (*Client, error) {
	// Connect to the server with insecure credentials (for simplicity)
	conn, err := stdgrpc.Dial(address, stdgrpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		return nil, fmt.Errorf("failed to connect to server at %s: %w", address, err)
	}

	// Create the gRPC client
	client := agateGrpc.NewSnapshotServiceClient(conn)

	return &Client{
		conn:   conn,
		client: client,
	}, nil
}

// Close closes the connection to the server
func (c *Client) Close() error {
	if c.conn != nil {
		return c.conn.Close()
	}
	return nil
}

// ListSnapshots retrieves a list of snapshots from the remote server
func (c *Client) ListSnapshots(ctx context.Context) ([]store.SnapshotInfo, error) {
	response, err := c.client.ListSnapshots(ctx, &agateGrpc.ListSnapshotsRequest{})
	if err != nil {
		return nil, fmt.Errorf("failed to list snapshots: %w", err)
	}

	// Convert gRPC snapshot info to store.SnapshotInfo
	snapshots := make([]store.SnapshotInfo, 0, len(response.Snapshots))
	for _, snapshot := range response.Snapshots {
		snapshots = append(snapshots, store.SnapshotInfo{
			ID:           snapshot.Id,
			Name:         snapshot.Name,
			ParentID:     snapshot.ParentId,
			CreationTime: snapshot.CreationTime.AsTime(),
		})
	}

	return snapshots, nil
}

// FetchSnapshotDetails retrieves detailed information about a specific snapshot
func (c *Client) FetchSnapshotDetails(ctx context.Context, snapshotID string) (*store.Snapshot, error) {
	response, err := c.client.GetSnapshotDetails(ctx, &agateGrpc.GetSnapshotDetailsRequest{
		SnapshotId: snapshotID,
	})
	if err != nil {
		return nil, fmt.Errorf("failed to get snapshot details: %w", err)
	}

	// Convert gRPC snapshot details to store.Snapshot
	snapshot := &store.Snapshot{
		ID:           response.Info.Id,
		Name:         response.Info.Name,
		ParentID:     response.Info.ParentId,
		CreationTime: response.Info.CreationTime.AsTime(),
		Files:        make([]store.FileInfo, 0, len(response.Files)),
	}

	// Convert file info
	for _, file := range response.Files {
		snapshot.Files = append(snapshot.Files, store.FileInfo{
			Path:   file.Path,
			Size:   file.SizeBytes,
			IsDir:  file.IsDir,
			SHA256: file.Sha256Hash,
		})
	}

	return snapshot, nil
}

// DownloadSnapshot downloads a snapshot from the server
// This implementation downloads each file individually to optimize bandwidth usage
func (c *Client) DownloadSnapshot(ctx context.Context, snapshotID string, targetDir string, localParentID string) error {
	// Get snapshot details
	snapshot, err := c.FetchSnapshotDetails(ctx, snapshotID)
	if err != nil {
		return fmt.Errorf("failed to get snapshot details: %w", err)
	}

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

	// If a local parent is specified, get its details to compare files
	var localParentFiles map[string]store.FileInfo
	if localParentID != "" {
		localParent, err := c.FetchSnapshotDetails(ctx, localParentID)
		if err == nil {
			// Create a map of file paths to file info for quick lookup
			localParentFiles = make(map[string]store.FileInfo, len(localParent.Files))
			for _, file := range localParent.Files {
				localParentFiles[file.Path] = file
			}
		}
	}

	// Download each file
	for _, file := range snapshot.Files {
		// Skip directories, we'll create them when needed
		if file.IsDir {
			// Create directory
			dirPath := filepath.Join(targetDir, file.Path)
			if err := os.MkdirAll(dirPath, 0755); err != nil {
				return fmt.Errorf("failed to create directory %s: %w", dirPath, err)
			}
			continue
		}

		// Check if we can skip downloading this file
		if localParentFiles != nil {
			if parentFile, exists := localParentFiles[file.Path]; exists && parentFile.SHA256 == file.SHA256 {
				// File exists in parent with same hash, copy it instead of downloading
				parentFilePath := filepath.Join(targetDir, "..", localParentID, file.Path)
				targetFilePath := filepath.Join(targetDir, file.Path)

				// Ensure parent directory exists
				if err := os.MkdirAll(filepath.Dir(targetFilePath), 0755); err != nil {
					return fmt.Errorf("failed to create directory for %s: %w", targetFilePath, err)
				}

				// Copy the file
				if err := copyFile(parentFilePath, targetFilePath); err != nil {
					// If copy fails, fall back to downloading
					fmt.Printf("Failed to copy file %s, will download instead: %v\n", file.Path, err)
				} else {
					// Skip to next file
					continue
				}
			}
		}

		// Download the file
		if err := c.downloadFile(ctx, snapshotID, file.Path, filepath.Join(targetDir, file.Path)); err != nil {
			return fmt.Errorf("failed to download file %s: %w", file.Path, err)
		}
	}

	return nil
}

// downloadFile downloads a single file from the server
func (c *Client) downloadFile(ctx context.Context, snapshotID, filePath, targetPath string) error {
	// Create the request
	req := &agateGrpc.DownloadFileRequest{
		SnapshotId: snapshotID,
		FilePath:   filePath,
	}

	// Start streaming the file
	stream, err := c.client.DownloadFile(ctx, req)
	if err != nil {
		return fmt.Errorf("failed to start file download: %w", err)
	}

	// Ensure the target directory exists
	if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
		return fmt.Errorf("failed to create directory for %s: %w", targetPath, err)
	}

	// Create the target file
	file, err := os.Create(targetPath)
	if err != nil {
		return fmt.Errorf("failed to create file %s: %w", targetPath, err)
	}
	defer file.Close()

	// Receive and write chunks
	for {
		resp, err := stream.Recv()
		if err == io.EOF {
			break
		}
		if err != nil {
			return fmt.Errorf("error receiving file chunk: %w", err)
		}

		// Write the chunk to the file
		if _, err := file.Write(resp.ChunkData); err != nil {
			return fmt.Errorf("error writing to file: %w", err)
		}
	}

	return nil
}

// Helper function to copy a file
func copyFile(src, dst string) error {
	sourceFile, err := os.Open(src)
	if err != nil {
		return err
	}
	defer sourceFile.Close()

	destFile, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer destFile.Close()

	_, err = io.Copy(destFile, sourceFile)
	return err
}

// Connect creates a new client connected to the specified address
func Connect(address string) (*Client, error) {
	return NewClient(address)
}