package rsync

import (
	"bytes"
	"context"
	"fmt"
	"log/slog"
	"net"
	"strconv"
	"time"

	rsynclib "github.com/Mzack9999/go-rsync/rsync"

	"github.com/praetorian-inc/fingerprintx/pkg/plugins"
	"github.com/praetorian-inc/fingerprintx/pkg/plugins/services/rsync"
	"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
)

type (
	// RsyncClient is a client for RSYNC servers.
	// Internally client uses https://github.com/gokrazy/rsync driver.
	// @example
	// ```javascript
	// const rsync = require('nuclei/rsync');
	// const client = new rsync.RsyncClient();
	// ```
	RsyncClient struct{}

	// IsRsyncResponse is the response from the IsRsync function.
	// this is returned by IsRsync function.
	// @example
	// ```javascript
	// const rsync = require('nuclei/rsync');
	// const isRsync = rsync.IsRsync('acme.com', 873);
	// log(toJSON(isRsync));
	// ```
	IsRsyncResponse struct {
		IsRsync bool
		Banner  string
	}

	// ListSharesResponse is the response from the ListShares function.
	// this is returned by ListShares function.
	// @example
	// ```javascript
	// const rsync = require('nuclei/rsync');
	// const client = new rsync.RsyncClient();
	// const listShares = client.ListShares('acme.com', 873);
	// log(toJSON(listShares));
	RsyncListResponse struct {
		Modules []string
		Files   []string
		Output  string
	}
)

func connectWithFastDialer(ctx context.Context, executionId string, host string, port int) (net.Conn, error) {
	dialer := protocolstate.GetDialersWithId(executionId)
	if dialer == nil {
		return nil, fmt.Errorf("dialers not initialized for %s", executionId)
	}
	return dialer.Fastdialer.Dial(ctx, "tcp", net.JoinHostPort(host, strconv.Itoa(port)))
}

// IsRsync checks if a host is running a Rsync server.
// @example
// ```javascript
// const rsync = require('nuclei/rsync');
// const isRsync = rsync.IsRsync('acme.com', 873);
// log(toJSON(isRsync));
// ```
func IsRsync(ctx context.Context, host string, port int) (IsRsyncResponse, error) {
	executionId := ctx.Value("executionId").(string)
	return memoizedisRsync(ctx, executionId, host, port)
}

// @memo
func isRsync(ctx context.Context, executionId string, host string, port int) (IsRsyncResponse, error) {
	resp := IsRsyncResponse{}

	timeout := 5 * time.Second
	conn, err := connectWithFastDialer(ctx, executionId, host, port)
	if err != nil {
		return resp, err
	}
	defer func() {
		_ = conn.Close()
	}()

	rsyncPlugin := rsync.RSYNCPlugin{}
	service, err := rsyncPlugin.Run(conn, timeout, plugins.Target{Host: host})
	if err != nil {
		return resp, nil
	}
	if service == nil {
		return resp, nil
	}
	resp.Banner = service.Version
	resp.IsRsync = true
	return resp, nil
}

// ListModules lists the modules of a Rsync server.
// @example
// ```javascript
// const rsync = require('nuclei/rsync');
// const client = new rsync.RsyncClient();
// const listModules = client.ListModules('acme.com', 873, 'username', 'password');
// log(toJSON(listModules));
// ```
func (c *RsyncClient) ListModules(ctx context.Context, host string, port int, username string, password string) (RsyncListResponse, error) {
	executionId := ctx.Value("executionId").(string)
	return listModules(executionId, host, port, username, password)
}

// ListShares lists the shares of a Rsync server.
// @example
// ```javascript
// const rsync = require('nuclei/rsync');
// const client = new rsync.RsyncClient();
// const listShares = client.ListFilesInModule('acme.com', 873, 'username', 'password', '/');
// log(toJSON(listShares));
// ```
func (c *RsyncClient) ListFilesInModule(ctx context.Context, host string, port int, username string, password string, module string) (RsyncListResponse, error) {
	executionId := ctx.Value("executionId").(string)
	return listFilesInModule(executionId, host, port, username, password, module)
}

func listModules(executionId string, host string, port int, username string, password string) (RsyncListResponse, error) {
	fastDialer := protocolstate.GetDialersWithId(executionId)
	if fastDialer == nil {
		return RsyncListResponse{}, fmt.Errorf("dialers not initialized for %s", executionId)
	}

	address := net.JoinHostPort(host, strconv.Itoa(port))

	// Create a bytes buffer for logging
	var logBuffer bytes.Buffer

	// Create a custom slog handler that writes to the buffer
	logHandler := slog.NewTextHandler(&logBuffer, &slog.HandlerOptions{
		Level: slog.LevelDebug,
	})

	// Create a logger that writes to our buffer
	logger := slog.New(logHandler)

	sr, err := rsynclib.ListModules(address,
		rsynclib.WithClientAuth(username, password),
		rsynclib.WithLogger(logger),
		rsynclib.WithFastDialer(fastDialer.Fastdialer),
	)
	if err != nil {
		return RsyncListResponse{}, fmt.Errorf("connect failed: %v", err)
	}

	result := RsyncListResponse{
		Modules: make([]string, len(sr)),
		Output:  logBuffer.String(),
	}

	for i, item := range sr {
		result.Modules[i] = string(item.Name)
	}

	return result, nil
}

func listFilesInModule(executionId string, host string, port int, username string, password string, module string) (RsyncListResponse, error) {
	fastDialer := protocolstate.GetDialersWithId(executionId)
	if fastDialer == nil {
		return RsyncListResponse{}, fmt.Errorf("dialers not initialized for %s", executionId)
	}

	address := net.JoinHostPort(host, strconv.Itoa(port))

	// Create a bytes buffer for logging
	var logBuffer bytes.Buffer

	// Create a custom slog handler that writes to the buffer
	logHandler := slog.NewTextHandler(&logBuffer, &slog.HandlerOptions{
		Level: slog.LevelDebug,
	})

	// Create a logger that writes to our buffer
	logger := slog.New(logHandler)

	sr, err := rsynclib.SocketClient(nil, address, module, ".",
		rsynclib.WithClientAuth(username, password),
		rsynclib.WithLogger(logger),
		rsynclib.WithFastDialer(fastDialer.Fastdialer),
	)
	if err != nil {
		return RsyncListResponse{}, fmt.Errorf("connect failed: %v", err)
	}

	// Try to list files to test authentication
	list, err := sr.List()
	if err != nil {
		return RsyncListResponse{}, fmt.Errorf("authentication failed: %v", err)
	}

	result := RsyncListResponse{
		Files:  make([]string, len(list)),
		Output: logBuffer.String(),
	}

	for i, item := range list {
		result.Files[i] = string(item.Path)
	}

	return result, nil
}
