package handlers

import (
	"bookhoard/internal/config"
	"bookhoard/internal/database"
	"bookhoard/internal/setupstatus"
	"encoding/json"
	"fmt"
	"net/http"
	"time"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5/pgtype"
	"github.com/labstack/echo/v5"
)

type SidecarHandler struct {
	db       *database.Queries
	cfg      *config.Config
	settings *database.SettingsRegistry
}

func NewSidecarHandler(db *database.Queries, cfg *config.Config) *SidecarHandler {
	return &SidecarHandler{db: db, cfg: cfg}
}

// SetSettings wires the tunable settings registry so the timezone write path
// keeps the cache consistent.
func (h *SidecarHandler) SetSettings(s *database.SettingsRegistry) { h.settings = s }

type SidecarConfig struct {
	Version        string                 `json:"version"`
	Bookhoard      SidecarBookhoardConfig `json:"bookhoard"`
	Books          map[string]SidecarBook `json:"books"`
	Collections    []SidecarCollection    `json:"collections"`
	OPDSEnabled    bool                   `json:"opds_enabled"`
	SidecarEnabled bool                   `json:"sidecar_enabled"`
	LastUpdated    string                 `json:"last_updated"`
}

type SidecarBookhoardConfig struct {
	OPDSCatalog string `json:"opds_catalog"`
	SyncAPI     string `json:"sync_api"`
	OPDSBaseURL string `json:"opds_base_url"`
	APIBaseURL  string `json:"api_base_url"`
	DeviceID    string `json:"device_id"`
	DeviceToken string `json:"device_token,omitempty"`
}

type SidecarBook struct {
	BookhoardUUID    string   `json:"bookhoard_uuid"`
	Title            string   `json:"title"`
	Author           string   `json:"author"`
	AvailableFormats []string `json:"available_formats"`
	SHA256           string   `json:"sha256,omitempty"`
	FilePath         string   `json:"file_path,omitempty"`
}

type SidecarCollection struct {
	Name         string   `json:"name"`
	ShelfMapping string   `json:"shelf_mapping,omitempty"`
	BookIDs      []string `json:"book_ids"`
}

// GetSidecarConfig generates and returns sidecar configuration for a device
// GET /api/devices/:device_id/sidecar
func (h *SidecarHandler) GetSidecarConfig(c *echo.Context) error {
	deviceID, err := uuid.Parse(c.Param("device_id"))
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{
			"error": "invalid device ID",
		})
	}

	ctx := c.Request().Context()
	pgDeviceID := pgtype.UUID{Bytes: deviceID, Valid: true}

	// Get device info
	device, err := h.db.GetDevice(ctx, pgDeviceID)
	if err != nil {
		return c.JSON(http.StatusNotFound, map[string]string{
			"error": "device not found",
		})
	}

	// Get user info
	userID := device.UserID.Bytes
	pgUserID := pgtype.UUID{Bytes: userID, Valid: true}

	// Get base URL and compute paths (with request-derived fallback)
	dbBaseURL, _ := h.db.GetSystemConfig(ctx, "base_url")
	baseURL := deriveBaseURL(c, dbBaseURL.Value)

	// Generate URLs
	opdsCatalogURL := fmt.Sprintf("%s/opds/devices/%s/catalog", baseURL, deviceID.String())
	syncAPIURL := fmt.Sprintf("%s/api/sync/kobo", baseURL)

	// Get user's visible libraries with media items
	mediaItems, err := h.db.GetUserMediaItemsForSync(ctx, pgUserID)
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{
			"error": "failed to fetch media items",
		})
	}

	// Build books map (keyed by SHA256, fallback to UUID)
	books := make(map[string]SidecarBook)
	for _, item := range mediaItems {
		bookUUID := uuid.UUID(item.ID.Bytes).String()

		// Use SHA256 as key if available, otherwise use UUID
		key := bookUUID
		if item.FileSha256.Valid && item.FileSha256.String != "" {
			key = item.FileSha256.String
		}

		availableFormats := []string{"epub"}
		if item.MimeType.Valid {
			if item.MimeType.String == "application/epub+zip" || item.MimeType.String == "application/octet-stream" {
				availableFormats = append(availableFormats, "kepub")
			}
		}

		author := ""
		if item.Author.Valid {
			author = item.Author.String
		}

		books[key] = SidecarBook{
			BookhoardUUID:    bookUUID,
			Title:            item.Title,
			Author:           author,
			AvailableFormats: availableFormats,
			SHA256:           item.FileSha256.String,
			FilePath:         item.FilePath,
		}

		// Also key the book by each per-format hash (KEPUB/PDF/...), so a device
		// holding a converted format resolves via the sidecar the same way it
		// would via BookResolver on the server.
		formats, ferr := h.db.GetMediaItemFormats(ctx, item.ID)
		if ferr == nil {
			entry := books[key]
			for _, f := range formats {
				if f.FileSha256.Valid && f.FileSha256.String != "" {
					if _, exists := books[f.FileSha256.String]; !exists {
						books[f.FileSha256.String] = entry
					}
				}
			}
		}
	}

	// Get collections
	collections, err := h.db.GetCollectionsByUser(ctx, pgUserID)
	if err != nil {
		// Non-fatal error, continue with empty collections
		collections = []database.Collections{}
	}

	// Build collections array
	sidecarCollections := []SidecarCollection{}
	for _, collection := range collections {
		// Get collection items
		collectionItems, err := h.db.GetCollectionItems(ctx, collection.ID)
		if err != nil {
			continue
		}

		bookIDs := make([]string, len(collectionItems))
		for i, item := range collectionItems {
			bookIDs[i] = uuid.UUID(item.MediaItemID.Bytes).String()
		}

		// Check for device shelf mapping
		shelfMapping := collection.Name
		mapping, err := h.db.GetDeviceShelfMapping(ctx, database.GetDeviceShelfMappingParams{
			DeviceID:     pgDeviceID,
			CollectionID: pgtype.UUID{Bytes: collection.ID.Bytes, Valid: true},
		})
		if err == nil && mapping.DeviceShelfName.Valid {
			shelfMapping = mapping.DeviceShelfName.String
		}

		sidecarCollections = append(sidecarCollections, SidecarCollection{
			Name:         collection.Name,
			ShelfMapping: shelfMapping,
			BookIDs:      bookIDs,
		})
	}

	// Build sidecar config
	sidecarConfig := SidecarConfig{
		Version: "1.0",
		Bookhoard: SidecarBookhoardConfig{
			OPDSCatalog: opdsCatalogURL,
			SyncAPI:     syncAPIURL,
			OPDSBaseURL: baseURL + "/opds",
			APIBaseURL:  baseURL + "/api",
			DeviceID:    deviceID.String(),
			DeviceToken: device.AuthToken,
		},
		Books:          books,
		Collections:    sidecarCollections,
		OPDSEnabled:    true,
		SidecarEnabled: true,
		LastUpdated:    time.Now().Format(time.RFC3339),
	}

	return c.JSON(http.StatusOK, sidecarConfig)
}

// DownloadSidecarConfig generates a .bookhoard.json file for device setup
// GET /api/devices/:device_id/sidecar/download
func (h *SidecarHandler) DownloadSidecarConfig(c *echo.Context) error {
	deviceID, err := uuid.Parse(c.Param("device_id"))
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{
			"error": "invalid device ID",
		})
	}

	ctx := c.Request().Context()
	pgDeviceID := pgtype.UUID{Bytes: deviceID, Valid: true}

	// Get device info to get device name
	device, err := h.db.GetDevice(ctx, pgDeviceID)
	if err != nil {
		return c.JSON(http.StatusNotFound, map[string]string{
			"error": "device not found",
		})
	}

	// Generate sidecar config
	var sidecarConfig SidecarConfig

	// Reuse GetSidecarConfig logic by building config inline
	userID := device.UserID.Bytes
	pgUserID := pgtype.UUID{Bytes: userID, Valid: true}

	// Get base URL and compute paths (with request-derived fallback)
	dbBaseURL, _ := h.db.GetSystemConfig(ctx, "base_url")
	baseURL := deriveBaseURL(c, dbBaseURL.Value)

	// Generate URLs
	opdsCatalogURL := fmt.Sprintf("%s/opds/devices/%s/catalog", baseURL, deviceID.String())
	syncAPIURL := fmt.Sprintf("%s/api/sync/kobo", baseURL)

	// Get user's visible libraries with media items
	mediaItems, err := h.db.GetUserMediaItemsForSync(ctx, pgUserID)
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{
			"error": "failed to fetch media items",
		})
	}

	// Build books map
	books := make(map[string]SidecarBook)
	for _, item := range mediaItems {
		bookUUID := uuid.UUID(item.ID.Bytes).String()

		key := bookUUID
		if item.FileSha256.Valid && item.FileSha256.String != "" {
			key = item.FileSha256.String
		}

		availableFormats := []string{"epub"}
		if item.MimeType.Valid {
			if item.MimeType.String == "application/epub+zip" || item.MimeType.String == "application/octet-stream" {
				availableFormats = append(availableFormats, "kepub")
			}
		}

		author := ""
		if item.Author.Valid {
			author = item.Author.String
		}

		books[key] = SidecarBook{
			BookhoardUUID:    bookUUID,
			Title:            item.Title,
			Author:           author,
			AvailableFormats: availableFormats,
			SHA256:           item.FileSha256.String,
			FilePath:         item.FilePath,
		}

		// Also key the book by each per-format hash (KEPUB/PDF/...), so a device
		// holding a converted format resolves via the sidecar the same way it
		// would via BookResolver on the server.
		formats, ferr := h.db.GetMediaItemFormats(ctx, item.ID)
		if ferr == nil {
			entry := books[key]
			for _, f := range formats {
				if f.FileSha256.Valid && f.FileSha256.String != "" {
					if _, exists := books[f.FileSha256.String]; !exists {
						books[f.FileSha256.String] = entry
					}
				}
			}
		}
	}

	// Get collections
	collections, err := h.db.GetCollectionsByUser(ctx, pgUserID)
	if err != nil {
		collections = []database.Collections{}
	}

	// Build collections array
	sidecarCollections := []SidecarCollection{}
	for _, collection := range collections {
		collectionItems, err := h.db.GetCollectionItems(ctx, collection.ID)
		if err != nil {
			continue
		}

		bookIDs := make([]string, len(collectionItems))
		for i, item := range collectionItems {
			bookIDs[i] = uuid.UUID(item.MediaItemID.Bytes).String()
		}

		shelfMapping := collection.Name
		mapping, err := h.db.GetDeviceShelfMapping(ctx, database.GetDeviceShelfMappingParams{
			DeviceID:     pgDeviceID,
			CollectionID: pgtype.UUID{Bytes: collection.ID.Bytes, Valid: true},
		})
		if err == nil && mapping.DeviceShelfName.Valid {
			shelfMapping = mapping.DeviceShelfName.String
		}

		sidecarCollections = append(sidecarCollections, SidecarCollection{
			Name:         collection.Name,
			ShelfMapping: shelfMapping,
			BookIDs:      bookIDs,
		})
	}

	sidecarConfig = SidecarConfig{
		Version: "1.0",
		Bookhoard: SidecarBookhoardConfig{
			OPDSCatalog: opdsCatalogURL,
			SyncAPI:     syncAPIURL,
			OPDSBaseURL: baseURL + "/opds",
			APIBaseURL:  baseURL + "/api",
			DeviceID:    deviceID.String(),
			DeviceToken: device.AuthToken,
		},
		Books:          books,
		Collections:    sidecarCollections,
		OPDSEnabled:    true,
		SidecarEnabled: true,
		LastUpdated:    time.Now().Format(time.RFC3339),
	}

	// Marshal to JSON with pretty formatting
	configJSON, err := json.MarshalIndent(sidecarConfig, "", "  ")
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{
			"error": "failed to generate config",
		})
	}

	// Set headers for file download
	filename := fmt.Sprintf("%s.bookhoard.json", sanitizeFilename(device.DeviceName))
	c.Response().Header().Set("Content-Type", "application/json")
	c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))

	return c.JSONBlob(http.StatusOK, configJSON)
}

// GetSystemConfiguration returns system-wide configuration
// GET /api/system/config
func (h *SidecarHandler) GetSystemConfiguration(c *echo.Context) error {
	ctx := c.Request().Context()

	// Get all system config
	configs, err := h.db.GetAllSystemConfig(ctx)
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{
			"error": "failed to fetch system config",
		})
	}

	// Build config map
	result := make(map[string]string)
	for _, systemConfig := range configs {
		result[systemConfig.Key] = systemConfig.Value
	}

	return c.JSON(http.StatusOK, result)
}

// UpdateSystemConfiguration updates system-wide configuration
// PUT /api/system/config
func (h *SidecarHandler) UpdateSystemConfiguration(c *echo.Context) error {
	user := c.Get("user")
	if user == nil {
		return c.JSON(http.StatusUnauthorized, map[string]string{
			"error": "unauthorized",
		})
	}

	userInfo := user.(database.Users)
	if userInfo.Role != "admin" {
		return c.JSON(http.StatusForbidden, map[string]string{
			"error": "admin access required",
		})
	}

	var req map[string]string
	if err := c.Bind(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{
			"error": "invalid request format",
		})
	}

	ctx := c.Request().Context()
	pgUserID := pgtype.UUID{Bytes: userInfo.ID.Bytes, Valid: true}

	// Update each config value
	for key, value := range req {
		if key == "default_timezone" {
			if _, err := time.LoadLocation(value); err != nil {
				return c.JSON(http.StatusBadRequest, map[string]string{
					"error": "invalid timezone",
				})
			}
			err := h.db.UpdateSystemSetting(ctx, database.UpdateSystemSettingParams{
				SettingKey:   "default_timezone",
				SettingValue: value,
			})
			if err != nil {
				return c.JSON(http.StatusInternalServerError, map[string]string{
					"error": "failed to update default timezone",
				})
			}
			if h.settings != nil {
				h.settings.Reload(ctx)
			}
			continue
		}
		_, err := h.db.SetSystemConfig(ctx, database.SetSystemConfigParams{
			Key:       key,
			Value:     value,
			UpdatedBy: pgUserID,
		})
		if err != nil {
			return c.JSON(http.StatusInternalServerError, map[string]string{
				"error": fmt.Sprintf("failed to update config key: %s", key),
			})
		}
	}

	if newBaseURL, ok := req["base_url"]; ok && newBaseURL != "" {
		derivedConfigs := map[string]string{
			"opds_base_url": newBaseURL + "/opds",
			"api_base_url":  newBaseURL + "/api",
		}
		for derivedKey, derivedValue := range derivedConfigs {
			_, err := h.db.SetSystemConfig(ctx, database.SetSystemConfigParams{
				Key:       derivedKey,
				Value:     derivedValue,
				UpdatedBy: pgUserID,
			})
			if err != nil {
				return c.JSON(http.StatusInternalServerError, map[string]string{
					"error": fmt.Sprintf("failed to update derived config key: %s", derivedKey),
				})
			}
		}

		// Invalidate setup status cache so the middleware picks up the new
		// base_url immediately (setup is not complete until base_url is set).
		setupstatus.Invalidate()
	}

	// Check for HTMX request
	if c.Request().Header.Get("HX-Request") == "true" {
		// Fetch updated base_url for template
		baseURL, err := h.db.GetSystemConfig(ctx, "base_url")
		if err != nil || baseURL.Value == "" {
			return c.HTML(http.StatusInternalServerError, `<div class="text-red-500">Failed to fetch updated configuration</div>`)
		}

		defaultTimezone := "UTC"
		tz, err := h.db.GetSystemTimezone(ctx)
		if err == nil && tz != "" {
			defaultTimezone = tz
		}

		// Render success message with updated form
		return c.HTML(http.StatusOK, fmt.Sprintf(`
<div class="mb-4 p-4 rounded-lg" style="background-color: var(--bg-secondary); border: 1px solid var(--accent);">
	<p style="color: var(--text-success);">✅ Settings saved successfully!</p>
</div>
<form id="settings-form" hx-put="/api/system/config" hx-target="#settings-form" hx-swap="outerHTML">
	<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
		<h3 class="text-xl font-semibold mb-6" style="color: var(--text-primary)">Base URL</h3>

		<div>
			<label class="block text-sm font-medium mb-2" style="color: var(--text-primary)">Base URL</label>
			<input
				type="url"
				name="base_url"
				value="%s"
				placeholder="https://books.example.com"
				class="w-full px-4 py-2 rounded-lg border"
				style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
				required
			/>
			<p class="text-sm mt-1" style="color: var(--text-secondary)">The public URL of your Bookhoard instance (e.g., https://books.example.com)</p>
		</div>

		<div class="mt-6 flex justify-end">
			<button type="submit" class="btn-primary px-6 py-2 rounded-lg font-medium">
				Save Settings
			</button>
		</div>
	</div>
	<div class="mt-8 card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
		<h3 class="text-xl font-semibold mb-6" style="color: var(--text-primary)">System Defaults</h3>
		<div>
			<label class="block text-sm font-medium mb-2" style="color: var(--text-primary)">Default Timezone</label>
			<select name="default_timezone" id="default_timezone" class="w-full px-4 py-2 rounded-lg border" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);">
				<option value="UTC"%s>UTC (UTC+0)</option>
				<option value="Pacific/Honolulu"%s>Hawaii (UTC-10)</option>
				<option value="America/Anchorage"%s>Alaska (UTC-9/-8)</option>
				<option value="America/Los_Angeles"%s>Pacific (UTC-8/-7)</option>
				<option value="America/Denver"%s>Mountain (UTC-7/-6)</option>
				<option value="America/Phoenix"%s>Mountain - no DST (UTC-7)</option>
				<option value="America/Chicago"%s>Central (UTC-6/-5)</option>
				<option value="America/New_York"%s>Eastern (UTC-5/-4)</option>
				<option value="America/Sao_Paulo"%s>Brasilia (UTC-3/-2)</option>
				<option value="Europe/London"%s>British (UTC+0/+1)</option>
				<option value="Europe/Paris"%s>Central European (UTC+1/+2)</option>
				<option value="Europe/Helsinki"%s>Eastern European (UTC+2/+3)</option>
				<option value="Europe/Moscow"%s>Moscow (UTC+3)</option>
				<option value="Asia/Tehran"%s>Iran (UTC+3:30)</option>
				<option value="Asia/Dubai"%s>Gulf (UTC+4)</option>
				<option value="Asia/Karachi"%s>Pakistan (UTC+5)</option>
				<option value="Asia/Kolkata"%s>India (UTC+5:30)</option>
				<option value="Asia/Dhaka"%s>Bangladesh (UTC+6)</option>
				<option value="Asia/Bangkok"%s>Indochina (UTC+7)</option>
				<option value="Asia/Shanghai"%s>China (UTC+8)</option>
				<option value="Asia/Tokyo"%s>Japan/Korea (UTC+9)</option>
				<option value="Australia/Darwin"%s>Australian Central (UTC+9:30)</option>
				<option value="Australia/Sydney"%s>Australian Eastern (UTC+10/+11)</option>
				<option value="Pacific/Auckland"%s>New Zealand (UTC+12/+13)</option>
			</select>
			<p class="text-sm mt-1" style="color: var(--text-secondary)">Default timezone for users who haven't set their own.</p>
		</div>
		<div class="mt-6 flex justify-end">
			<button type="submit" class="btn-primary px-6 py-2 rounded-lg font-medium">
				Save Settings
			</button>
		</div>
	</div>
</form>

<div class="mt-8 card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
	<h3 class="text-xl font-semibold mb-4" style="color: var(--text-primary)">URL Paths</h3>
	<div class="space-y-2 text-sm" style="color: var(--text-secondary);">
		<p><strong>OPDS:</strong> %s/opds</p>
		<p><strong>API:</strong> %s/api</p>
		<p><strong>Device Sync:</strong> %s/api/sync</p>
	</div>
</div>
`, baseURL.Value,
			selectedAttr(defaultTimezone, "UTC"),
			selectedAttr(defaultTimezone, "Pacific/Honolulu"),
			selectedAttr(defaultTimezone, "America/Anchorage"),
			selectedAttr(defaultTimezone, "America/Los_Angeles"),
			selectedAttr(defaultTimezone, "America/Denver"),
			selectedAttr(defaultTimezone, "America/Phoenix"),
			selectedAttr(defaultTimezone, "America/Chicago"),
			selectedAttr(defaultTimezone, "America/New_York"),
			selectedAttr(defaultTimezone, "America/Sao_Paulo"),
			selectedAttr(defaultTimezone, "Europe/London"),
			selectedAttr(defaultTimezone, "Europe/Paris"),
			selectedAttr(defaultTimezone, "Europe/Helsinki"),
			selectedAttr(defaultTimezone, "Europe/Moscow"),
			selectedAttr(defaultTimezone, "Asia/Tehran"),
			selectedAttr(defaultTimezone, "Asia/Dubai"),
			selectedAttr(defaultTimezone, "Asia/Karachi"),
			selectedAttr(defaultTimezone, "Asia/Kolkata"),
			selectedAttr(defaultTimezone, "Asia/Dhaka"),
			selectedAttr(defaultTimezone, "Asia/Bangkok"),
			selectedAttr(defaultTimezone, "Asia/Shanghai"),
			selectedAttr(defaultTimezone, "Asia/Tokyo"),
			selectedAttr(defaultTimezone, "Australia/Darwin"),
			selectedAttr(defaultTimezone, "Australia/Sydney"),
			selectedAttr(defaultTimezone, "Pacific/Auckland"),
			baseURL.Value, baseURL.Value, baseURL.Value))
	}

	return c.JSON(http.StatusOK, map[string]string{
		"status":  "success",
		"message": "System configuration updated",
	})
}

func sanitizeFilename(name string) string {
	// Simple sanitization - replace problematic characters
	sanitized := name
	for _, ch := range []string{"/", "\\", ":", "*", "?", "\"", "<", ">", "|"} {
		sanitized = sanitizeAll(sanitized, ch, "_")
	}
	return sanitized
}

func sanitizeAll(s string, old string, new string) string {
	result := ""
	for _, ch := range s {
		c := string(ch)
		if c == old {
			result += new
		} else {
			result += c
		}
	}
	return result
}

func selectedAttr(current, value string) string {
	if current == value {
		return " selected"
	}
	return ""
}
