package service

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"log/slog"
	"strings"
	"time"

	"github.com/TopherMayor/unified-media-manager/internal/db"
	"github.com/TopherMayor/unified-media-manager/internal/download"
)

type DownloadClientConfig struct {
	ID             int64           `json:"id"`
	Name           string          `json:"name"`
	Implementation string          `json:"implementation"`
	URL            string          `json:"url"`
	APIKey         *string         `json:"-"`
	Category       string          `json:"category"`
	Priority       int             `json:"priority"`
	Protocol       string          `json:"protocol"`
	Settings       json.RawMessage `json:"settings"`
	Enabled        bool            `json:"enabled"`
	CreatedAt      time.Time       `json:"created_at"`
	UpdatedAt      time.Time       `json:"updated_at"`
}

type DownloadClientConfigResponse struct {
	ID             int64           `json:"id"`
	Name           string          `json:"name"`
	Implementation string          `json:"implementation"`
	URL            string          `json:"url"`
	Category       string          `json:"category"`
	Priority       int             `json:"priority"`
	Protocol       string          `json:"protocol"`
	Settings       json.RawMessage `json:"settings"`
	Enabled        bool            `json:"enabled"`
	CreatedAt      time.Time       `json:"created_at"`
	UpdatedAt      time.Time       `json:"updated_at"`
}

type DownloadClientWithInfo struct {
	Client download.DownloadClient
	Config DownloadClientConfig
}

type CreateDownloadClientRequest struct {
	Name           string          `json:"name"`
	Implementation string          `json:"implementation"`
	URL            string          `json:"url"`
	APIKey         *string         `json:"api_key,omitempty"`
	Category       string          `json:"category,omitempty"`
	Priority       *int            `json:"priority,omitempty"`
	Protocol       string          `json:"protocol,omitempty"`
	Settings       json.RawMessage `json:"settings,omitempty"`
	Enabled        *bool           `json:"enabled,omitempty"`
}

type UpdateDownloadClientRequest struct {
	Name           *string         `json:"name,omitempty"`
	Implementation *string         `json:"implementation,omitempty"`
	URL            *string         `json:"url,omitempty"`
	APIKey         *string         `json:"api_key,omitempty"`
	Category       *string         `json:"category,omitempty"`
	Priority       *int            `json:"priority,omitempty"`
	Protocol       *string         `json:"protocol,omitempty"`
	Settings       json.RawMessage `json:"settings,omitempty"`
	Enabled        *bool           `json:"enabled,omitempty"`
}

type DownloadClientTestResult struct {
	Success bool   `json:"success"`
	Error   string `json:"error,omitempty"`
}

const downloadClientColumns = `id, name, implementation, url, api_key, category, priority, protocol, settings, enabled, created_at, updated_at`

type DownloadClientService struct {
	db *db.DB
}

func NewDownloadClientService(database *db.DB) *DownloadClientService {
	return &DownloadClientService{db: database}
}

func scanDownloadClientConfig(scanner interface{ Scan(...interface{}) error }) (*DownloadClientConfig, error) {
	var cfg DownloadClientConfig
	var apiKey sql.NullString
	var settings []byte

	err := scanner.Scan(&cfg.ID, &cfg.Name, &cfg.Implementation, &cfg.URL, &apiKey,
		&cfg.Category, &cfg.Priority, &cfg.Protocol, &settings,
		&cfg.Enabled, &cfg.CreatedAt, &cfg.UpdatedAt)
	if err != nil {
		return nil, err
	}

	if apiKey.Valid {
		cfg.APIKey = &apiKey.String
	}
	cfg.Settings = json.RawMessage(settings)

	return &cfg, nil
}

func clientConfigToResponse(cfg *DownloadClientConfig) DownloadClientConfigResponse {
	return DownloadClientConfigResponse{
		ID:             cfg.ID,
		Name:           cfg.Name,
		Implementation: cfg.Implementation,
		URL:            cfg.URL,
		Category:       cfg.Category,
		Priority:       cfg.Priority,
		Protocol:       cfg.Protocol,
		Settings:       cfg.Settings,
		Enabled:        cfg.Enabled,
		CreatedAt:      cfg.CreatedAt,
		UpdatedAt:      cfg.UpdatedAt,
	}
}

func (s *DownloadClientService) List(ctx context.Context) ([]DownloadClientConfigResponse, error) {
	rows, err := s.db.Pool.Query(ctx,
		fmt.Sprintf("SELECT %s FROM download_clients ORDER BY priority, name", downloadClientColumns))
	if err != nil {
		return nil, fmt.Errorf("list download clients: %w", err)
	}
	defer rows.Close()

	var items []DownloadClientConfigResponse
	for rows.Next() {
		cfg, err := scanDownloadClientConfig(rows)
		if err != nil {
			slog.Error("failed to scan download client", "error", err)
			continue
		}
		items = append(items, clientConfigToResponse(cfg))
	}

	return items, nil
}

func (s *DownloadClientService) GetByID(ctx context.Context, id int64) (*DownloadClientConfig, error) {
	row := s.db.Pool.QueryRow(ctx,
		fmt.Sprintf("SELECT %s FROM download_clients WHERE id = $1", downloadClientColumns), id)

	cfg, err := scanDownloadClientConfig(row)
	if err != nil {
		return nil, fmt.Errorf("download client not found")
	}

	return cfg, nil
}

func (s *DownloadClientService) Create(ctx context.Context, req CreateDownloadClientRequest) (int64, error) {
	category := req.Category
	if category == "" {
		category = "umm"
	}
	protocol := req.Protocol
	if protocol == "" {
		switch req.Implementation {
		case "sabnzbd":
			protocol = "nzb"
		case "qbittorrent":
			protocol = "torrent"
		default:
			protocol = "nzb"
		}
	}
	settings := req.Settings
	if settings == nil {
		settings = json.RawMessage("{}")
	}
	enabled := true
	if req.Enabled != nil {
		enabled = *req.Enabled
	}
	priority := 0
	if req.Priority != nil {
		priority = *req.Priority
	}

	var id int64
	err := s.db.Pool.QueryRow(ctx,
		`INSERT INTO download_clients (name, implementation, url, api_key, category, priority, protocol, settings, enabled)
		 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id`,
		req.Name, req.Implementation, req.URL, req.APIKey, category, priority, protocol, settings, enabled).Scan(&id)
	if err != nil {
		return 0, fmt.Errorf("create download client: %w", err)
	}

	return id, nil
}

func (s *DownloadClientService) Update(ctx context.Context, id int64, req UpdateDownloadClientRequest) error {
	var setClauses []string
	var args []interface{}
	idx := 1

	addCol := func(col string, val interface{}) {
		setClauses = append(setClauses, fmt.Sprintf("%s = $%d", col, idx))
		args = append(args, val)
		idx++
	}

	if req.Name != nil {
		addCol("name", *req.Name)
	}
	if req.Implementation != nil {
		addCol("implementation", *req.Implementation)
	}
	if req.URL != nil {
		addCol("url", *req.URL)
	}
	if req.APIKey != nil {
		addCol("api_key", *req.APIKey)
	}
	if req.Category != nil {
		addCol("category", *req.Category)
	}
	if req.Priority != nil {
		addCol("priority", *req.Priority)
	}
	if req.Protocol != nil {
		addCol("protocol", *req.Protocol)
	}
	if req.Settings != nil {
		addCol("settings", req.Settings)
	}
	if req.Enabled != nil {
		addCol("enabled", *req.Enabled)
	}

	if len(setClauses) == 0 {
		return fmt.Errorf("no fields to update")
	}

	addCol("updated_at", time.Now())

	query := fmt.Sprintf("UPDATE download_clients SET %s WHERE id = $%d",
		strings.Join(setClauses, ", "), idx)
	args = append(args, id)

	tag, err := s.db.Pool.Exec(ctx, query, args...)
	if err != nil {
		return fmt.Errorf("update download client: %w", err)
	}
	if tag.RowsAffected() == 0 {
		return fmt.Errorf("download client not found")
	}

	return nil
}

func (s *DownloadClientService) Delete(ctx context.Context, id int64) error {
	tag, err := s.db.Pool.Exec(ctx, "DELETE FROM download_clients WHERE id = $1", id)
	if err != nil {
		return fmt.Errorf("delete download client: %w", err)
	}
	if tag.RowsAffected() == 0 {
		return fmt.Errorf("download client not found")
	}
	return nil
}

func (s *DownloadClientService) GetClient(ctx context.Context, protocol string) (download.DownloadClient, *DownloadClientConfig, error) {
	row := s.db.Pool.QueryRow(ctx,
		fmt.Sprintf("SELECT %s FROM download_clients WHERE enabled = true AND protocol = $1 ORDER BY priority ASC LIMIT 1", downloadClientColumns),
		protocol)

	cfg, err := scanDownloadClientConfig(row)
	if err != nil {
		return nil, nil, fmt.Errorf("no enabled download client for protocol: %s", protocol)
	}

	client, err := s.instantiateClient(cfg)
	if err != nil {
		return nil, nil, err
	}

	return client, cfg, nil
}

func (s *DownloadClientService) GetAllEnabled(ctx context.Context, protocol string) ([]DownloadClientWithInfo, error) {
	rows, err := s.db.Pool.Query(ctx,
		fmt.Sprintf("SELECT %s FROM download_clients WHERE enabled = true AND protocol = $1 ORDER BY priority ASC", downloadClientColumns),
		protocol)
	if err != nil {
		return nil, fmt.Errorf("list enabled download clients: %w", err)
	}
	defer rows.Close()

	var clients []DownloadClientWithInfo
	for rows.Next() {
		cfg, err := scanDownloadClientConfig(rows)
		if err != nil {
			slog.Error("failed to scan download client", "error", err)
			continue
		}

		client, err := s.instantiateClient(cfg)
		if err != nil {
			slog.Error("failed to instantiate download client", "error", err, "name", cfg.Name)
			continue
		}

		clients = append(clients, DownloadClientWithInfo{
			Client: client,
			Config: *cfg,
		})
	}

	return clients, nil
}

func (s *DownloadClientService) Test(ctx context.Context, id int64) (*DownloadClientTestResult, error) {
	cfg, err := s.GetByID(ctx, id)
	if err != nil {
		return nil, err
	}

	client, err := s.instantiateClient(cfg)
	if err != nil {
		return &DownloadClientTestResult{Success: false, Error: err.Error()}, nil
	}

	_, err = client.GetCompleted(ctx)
	if err != nil {
		return &DownloadClientTestResult{Success: false, Error: err.Error()}, nil
	}

	return &DownloadClientTestResult{Success: true}, nil
}

func (s *DownloadClientService) instantiateClient(cfg *DownloadClientConfig) (download.DownloadClient, error) {
	apiKey := ""
	if cfg.APIKey != nil {
		apiKey = *cfg.APIKey
	}

	switch cfg.Implementation {
	case "sabnzbd":
		return download.NewSABnzbdClient(cfg.URL, apiKey), nil
	case "qbittorrent":
		return download.NewQBittorrentClient(cfg.URL, apiKey), nil
	default:
		return nil, fmt.Errorf("unknown download client implementation: %s", cfg.Implementation)
	}
}
