package fail2ban

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"path"
	"strconv"
	"strings"
	"time"

	"github.com/swissmakers/fail2ban-ui/internal/config"
)

// AgentConnector connects to a remote fail2ban-agent via HTTP API.
type AgentConnector struct {
	server config.Fail2banServer
	base   *url.URL
	client *http.Client
}

// NewAgentConnector constructs a new AgentConnector.
func NewAgentConnector(server config.Fail2banServer) (Connector, error) {
	if server.AgentURL == "" {
		return nil, fmt.Errorf("agentUrl is required for agent connector")
	}
	if server.AgentSecret == "" {
		return nil, fmt.Errorf("agentSecret is required for agent connector")
	}
	parsed, err := url.Parse(server.AgentURL)
	if err != nil {
		return nil, fmt.Errorf("invalid agentUrl: %w", err)
	}
	if parsed.Scheme == "" {
		parsed.Scheme = "https"
	}
	client := &http.Client{
		Timeout: 15 * time.Second,
	}
	conn := &AgentConnector{
		server: server,
		base:   parsed,
		client: client,
	}
	if err := conn.ensureAction(context.Background()); err != nil {
		fmt.Printf("warning: failed to ensure agent action for %s: %v\n", server.Name, err)
	}
	return conn, nil
}

func (ac *AgentConnector) ID() string {
	return ac.server.ID
}

func (ac *AgentConnector) Server() config.Fail2banServer {
	return ac.server
}

func (ac *AgentConnector) ensureAction(ctx context.Context) error {
	settings := config.GetSettings()
	payload := map[string]any{
		"name":        "ui-custom-action",
		"config":      config.BuildFail2banActionConfig(config.GetCallbackURL(), ac.server.ID, settings.CallbackSecret),
		"callbackUrl": config.GetCallbackURL(),
		"setDefault":  true,
	}
	return ac.put(ctx, "/v1/actions/ui-custom", payload, nil)
}

func (ac *AgentConnector) GetJailInfos(ctx context.Context) ([]JailInfo, error) {
	var resp struct {
		Jails []JailInfo `json:"jails"`
	}
	if err := ac.get(ctx, "/v1/jails", &resp); err != nil {
		return nil, err
	}
	return resp.Jails, nil
}

func (ac *AgentConnector) GetBannedIPs(ctx context.Context, jail string) ([]string, error) {
	var resp struct {
		Jail        string   `json:"jail"`
		BannedIPs   []string `json:"bannedIPs"`
		TotalBanned int      `json:"totalBanned"`
	}
	if err := ac.get(ctx, fmt.Sprintf("/v1/jails/%s", url.PathEscape(jail)), &resp); err != nil {
		return nil, err
	}
	if len(resp.BannedIPs) > 0 {
		return resp.BannedIPs, nil
	}
	return []string{}, nil
}

func (ac *AgentConnector) UnbanIP(ctx context.Context, jail, ip string) error {
	payload := map[string]string{"ip": ip}
	return ac.post(ctx, fmt.Sprintf("/v1/jails/%s/unban", url.PathEscape(jail)), payload, nil)
}

func (ac *AgentConnector) BanIP(ctx context.Context, jail, ip string) error {
	payload := map[string]string{"ip": ip}
	return ac.post(ctx, fmt.Sprintf("/v1/jails/%s/ban", url.PathEscape(jail)), payload, nil)
}

func (ac *AgentConnector) Reload(ctx context.Context) error {
	return ac.post(ctx, "/v1/actions/reload", nil, nil)
}

func (ac *AgentConnector) Restart(ctx context.Context) error {
	return ac.post(ctx, "/v1/actions/restart", nil, nil)
}

// RestartWithMode restarts the remote agent-managed Fail2ban service and
// always reports mode "restart". Any error is propagated to the caller.
func (ac *AgentConnector) RestartWithMode(ctx context.Context) (string, error) {
	if err := ac.Restart(ctx); err != nil {
		return "restart", err
	}
	return "restart", nil
}

func (ac *AgentConnector) GetFilterConfig(ctx context.Context, jail string) (string, string, error) {
	var resp struct {
		Config   string `json:"config"`
		FilePath string `json:"filePath"`
	}
	if err := ac.get(ctx, fmt.Sprintf("/v1/filters/%s", url.PathEscape(jail)), &resp); err != nil {
		return "", "", err
	}
	// If agent doesn't return filePath, construct it (agent should handle .local priority)
	filePath := resp.FilePath
	if filePath == "" {
		// Default to .local path (agent should handle .local priority on its side)
		filePath = fmt.Sprintf("/etc/fail2ban/filter.d/%s.local", jail)
	}
	return resp.Config, filePath, nil
}

func (ac *AgentConnector) SetFilterConfig(ctx context.Context, jail, content string) error {
	payload := map[string]string{"config": content}
	return ac.put(ctx, fmt.Sprintf("/v1/filters/%s", url.PathEscape(jail)), payload, nil)
}

func (ac *AgentConnector) FetchBanEvents(ctx context.Context, limit int) ([]BanEvent, error) {
	query := url.Values{}
	if limit > 0 {
		query.Set("limit", strconv.Itoa(limit))
	}
	var resp struct {
		Events []struct {
			IP        string `json:"ip"`
			Jail      string `json:"jail"`
			Hostname  string `json:"hostname"`
			Failures  string `json:"failures"`
			Whois     string `json:"whois"`
			Logs      string `json:"logs"`
			Timestamp string `json:"timestamp"`
		} `json:"events"`
	}
	endpoint := "/v1/events"
	if encoded := query.Encode(); encoded != "" {
		endpoint += "?" + encoded
	}
	if err := ac.get(ctx, endpoint, &resp); err != nil {
		return nil, err
	}
	result := make([]BanEvent, 0, len(resp.Events))
	for _, evt := range resp.Events {
		ts, err := time.Parse(time.RFC3339, evt.Timestamp)
		if err != nil {
			ts = time.Now()
		}
		result = append(result, BanEvent{
			Time:    ts,
			Jail:    evt.Jail,
			IP:      evt.IP,
			LogLine: fmt.Sprintf("%s %s", evt.Hostname, evt.Failures),
		})
	}
	return result, nil
}

func (ac *AgentConnector) get(ctx context.Context, endpoint string, out any) error {
	req, err := ac.newRequest(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return err
	}
	return ac.do(req, out)
}

func (ac *AgentConnector) post(ctx context.Context, endpoint string, payload any, out any) error {
	req, err := ac.newRequest(ctx, http.MethodPost, endpoint, payload)
	if err != nil {
		return err
	}
	return ac.do(req, out)
}

func (ac *AgentConnector) put(ctx context.Context, endpoint string, payload any, out any) error {
	req, err := ac.newRequest(ctx, http.MethodPut, endpoint, payload)
	if err != nil {
		return err
	}
	return ac.do(req, out)
}

func (ac *AgentConnector) delete(ctx context.Context, endpoint string, out any) error {
	req, err := ac.newRequest(ctx, http.MethodDelete, endpoint, nil)
	if err != nil {
		return err
	}
	return ac.do(req, out)
}

func (ac *AgentConnector) newRequest(ctx context.Context, method, endpoint string, payload any) (*http.Request, error) {
	u := *ac.base
	u.Path = path.Join(ac.base.Path, strings.TrimPrefix(endpoint, "/"))

	var body io.Reader
	if payload != nil {
		data, err := json.Marshal(payload)
		if err != nil {
			return nil, err
		}
		body = bytes.NewReader(data)
	}

	req, err := http.NewRequestWithContext(ctx, method, u.String(), body)
	if err != nil {
		return nil, err
	}
	if payload != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	req.Header.Set("Accept", "application/json")
	req.Header.Set("X-F2B-Token", ac.server.AgentSecret)
	return req, nil
}

func (ac *AgentConnector) do(req *http.Request, out any) error {
	settingsSnapshot := config.GetSettings()
	if settingsSnapshot.Debug {
		config.DebugLog("Agent request [%s]: %s %s", ac.server.Name, req.Method, req.URL.String())
	}

	resp, err := ac.client.Do(req)
	if err != nil {
		if settingsSnapshot.Debug {
			config.DebugLog("Agent request error [%s]: %v", ac.server.Name, err)
		}
		return fmt.Errorf("agent request failed: %w", err)
	}
	defer resp.Body.Close()

	data, err := io.ReadAll(io.LimitReader(resp.Body, 4096))
	if err != nil {
		return err
	}
	trimmed := strings.TrimSpace(string(data))

	if settingsSnapshot.Debug {
		config.DebugLog("Agent response [%s]: %s | %s", ac.server.Name, resp.Status, trimmed)
	}

	if resp.StatusCode >= 400 {
		return fmt.Errorf("agent request failed: %s (%s)", resp.Status, trimmed)
	}

	if out == nil {
		return nil
	}

	if len(trimmed) == 0 {
		return nil
	}
	return json.Unmarshal(data, out)
}

// GetAllJails implements Connector.
func (ac *AgentConnector) GetAllJails(ctx context.Context) ([]JailInfo, error) {
	var resp struct {
		Jails []JailInfo `json:"jails"`
	}
	if err := ac.get(ctx, "/v1/jails/all", &resp); err != nil {
		return nil, err
	}
	return resp.Jails, nil
}

// UpdateJailEnabledStates implements Connector.
func (ac *AgentConnector) UpdateJailEnabledStates(ctx context.Context, updates map[string]bool) error {
	return ac.post(ctx, "/v1/jails/update-enabled", updates, nil)
}

// GetFilters implements Connector.
func (ac *AgentConnector) GetFilters(ctx context.Context) ([]string, error) {
	var resp struct {
		Filters []string `json:"filters"`
	}
	if err := ac.get(ctx, "/v1/filters", &resp); err != nil {
		return nil, err
	}
	return resp.Filters, nil
}

// TestFilter implements Connector.
func (ac *AgentConnector) TestFilter(ctx context.Context, filterName string, logLines []string, filterContent string) (string, string, error) {
	payload := map[string]any{
		"filterName": filterName,
		"logLines":   logLines,
	}
	if filterContent != "" {
		payload["filterContent"] = filterContent
	}
	var resp struct {
		Output     string `json:"output"`
		FilterPath string `json:"filterPath"`
	}
	if err := ac.post(ctx, "/v1/filters/test", payload, &resp); err != nil {
		return "", "", err
	}
	// If agent doesn't return filterPath, construct it (agent should handle .local priority)
	filterPath := resp.FilterPath
	if filterPath == "" {
		// Default to .conf path (agent should handle .local priority on its side)
		filterPath = fmt.Sprintf("/etc/fail2ban/filter.d/%s.conf", filterName)
	}
	return resp.Output, filterPath, nil
}

// GetJailConfig implements Connector.
func (ac *AgentConnector) GetJailConfig(ctx context.Context, jail string) (string, string, error) {
	var resp struct {
		Config   string `json:"config"`
		FilePath string `json:"filePath"`
	}
	if err := ac.get(ctx, fmt.Sprintf("/v1/jails/%s/config", url.PathEscape(jail)), &resp); err != nil {
		return "", "", err
	}
	// If agent doesn't return filePath, construct it (agent should handle .local priority)
	filePath := resp.FilePath
	if filePath == "" {
		// Default to .local path (agent should handle .local priority on its side)
		filePath = fmt.Sprintf("/etc/fail2ban/jail.d/%s.local", jail)
	}
	return resp.Config, filePath, nil
}

// SetJailConfig implements Connector.
func (ac *AgentConnector) SetJailConfig(ctx context.Context, jail, content string) error {
	payload := map[string]string{"config": content}
	return ac.put(ctx, fmt.Sprintf("/v1/jails/%s/config", url.PathEscape(jail)), payload, nil)
}

// TestLogpath implements Connector.
func (ac *AgentConnector) TestLogpath(ctx context.Context, logpath string) ([]string, error) {
	payload := map[string]string{"logpath": logpath}
	var resp struct {
		Files []string `json:"files"`
	}
	if err := ac.post(ctx, "/v1/jails/test-logpath", payload, &resp); err != nil {
		return []string{}, nil // Return empty on error
	}
	return resp.Files, nil
}

// TestLogpathWithResolution implements Connector.
// Agent server should handle variable resolution.
func (ac *AgentConnector) TestLogpathWithResolution(ctx context.Context, logpath string) (originalPath, resolvedPath string, files []string, err error) {
	originalPath = strings.TrimSpace(logpath)
	if originalPath == "" {
		return originalPath, "", []string{}, nil
	}

	payload := map[string]string{"logpath": originalPath}
	var resp struct {
		OriginalLogpath string   `json:"original_logpath"`
		ResolvedLogpath string   `json:"resolved_logpath"`
		Files           []string `json:"files"`
		Error           string   `json:"error,omitempty"`
	}

	// Try new endpoint first, fallback to old endpoint
	if err := ac.post(ctx, "/v1/jails/test-logpath-with-resolution", payload, &resp); err != nil {
		// Fallback: use old endpoint and assume no resolution
		files, err2 := ac.TestLogpath(ctx, originalPath)
		if err2 != nil {
			return originalPath, "", nil, fmt.Errorf("failed to test logpath: %w", err2)
		}
		return originalPath, originalPath, files, nil
	}

	if resp.Error != "" {
		return originalPath, "", nil, fmt.Errorf("agent error: %s", resp.Error)
	}

	if resp.ResolvedLogpath == "" {
		resp.ResolvedLogpath = resp.OriginalLogpath
	}
	if resp.OriginalLogpath == "" {
		resp.OriginalLogpath = originalPath
	}

	return resp.OriginalLogpath, resp.ResolvedLogpath, resp.Files, nil
}

// UpdateDefaultSettings implements Connector.
func (ac *AgentConnector) UpdateDefaultSettings(ctx context.Context, settings config.AppSettings) error {
	// Convert IgnoreIPs array to space-separated string
	ignoreIPStr := strings.Join(settings.IgnoreIPs, " ")
	if ignoreIPStr == "" {
		ignoreIPStr = "127.0.0.1/8 ::1"
	}
	// Set default banaction values if not set
	banaction := settings.Banaction
	if banaction == "" {
		banaction = "nftables-multiport"
	}
	banactionAllports := settings.BanactionAllports
	if banactionAllports == "" {
		banactionAllports = "nftables-allports"
	}
	payload := map[string]interface{}{
		"bantimeIncrement":  settings.BantimeIncrement,
		"defaultJailEnable": settings.DefaultJailEnable,
		"ignoreip":          ignoreIPStr,
		"bantime":           settings.Bantime,
		"findtime":          settings.Findtime,
		"maxretry":          settings.Maxretry,
		"banaction":         banaction,
		"banactionAllports": banactionAllports,
	}
	return ac.put(ctx, "/v1/jails/default-settings", payload, nil)
}

// EnsureJailLocalStructure implements Connector.
func (ac *AgentConnector) EnsureJailLocalStructure(ctx context.Context) error {
	// Call agent API endpoint to ensure jail.local structure
	// If the endpoint doesn't exist, we'll need to implement it on the agent side
	// For now, we'll try calling it and handle the error gracefully
	return ac.post(ctx, "/v1/jails/ensure-structure", nil, nil)
}

// CreateJail implements Connector.
func (ac *AgentConnector) CreateJail(ctx context.Context, jailName, content string) error {
	payload := map[string]interface{}{
		"name":    jailName,
		"content": content,
	}
	return ac.post(ctx, "/v1/jails", payload, nil)
}

// DeleteJail implements Connector.
func (ac *AgentConnector) DeleteJail(ctx context.Context, jailName string) error {
	return ac.delete(ctx, fmt.Sprintf("/v1/jails/%s", jailName), nil)
}

// CreateFilter implements Connector.
func (ac *AgentConnector) CreateFilter(ctx context.Context, filterName, content string) error {
	payload := map[string]interface{}{
		"name":    filterName,
		"content": content,
	}
	return ac.post(ctx, "/v1/filters", payload, nil)
}

// DeleteFilter implements Connector.
func (ac *AgentConnector) DeleteFilter(ctx context.Context, filterName string) error {
	return ac.delete(ctx, fmt.Sprintf("/v1/filters/%s", filterName), nil)
}
