package api

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
	"sync"

	"git.fabledsword.com/bvandeusen/minstrel/internal/lidarr"
	"git.fabledsword.com/bvandeusen/minstrel/internal/lidarrconfig"
)

// lidarrConfigView is the JSON shape returned by GET /api/admin/lidarr/config
// and PUT /api/admin/lidarr/config. api_key is always masked as "***" when set.
type lidarrConfigView struct {
	Enabled                  bool   `json:"enabled"`
	BaseURL                  string `json:"base_url"`
	APIKey                   string `json:"api_key"`
	DefaultQualityProfileID  int    `json:"default_quality_profile_id"`
	DefaultMetadataProfileID int    `json:"default_metadata_profile_id"`
	DefaultRootFolderPath    string `json:"default_root_folder_path"`
}

// maskAPIKey converts an api_key for external response: non-empty keys become
// "***"; empty (never set) keys remain "".
func maskAPIKey(key string) string {
	if key == "" {
		return ""
	}
	return "***"
}

// configToView converts a lidarrconfig.Config to a lidarrConfigView with the
// api_key masked.
func configToView(cfg lidarrconfig.Config) lidarrConfigView {
	return lidarrConfigView{
		Enabled:                  cfg.Enabled,
		BaseURL:                  cfg.BaseURL,
		APIKey:                   maskAPIKey(cfg.APIKey),
		DefaultQualityProfileID:  cfg.DefaultQualityProfileID,
		DefaultMetadataProfileID: cfg.DefaultMetadataProfileID,
		DefaultRootFolderPath:    cfg.DefaultRootFolderPath,
	}
}

// handleGetLidarrConfig implements GET /api/admin/lidarr/config.
// Returns the current Lidarr config with api_key masked.
func (h *handlers) handleGetLidarrConfig(w http.ResponseWriter, r *http.Request) {
	cfg, err := h.lidarrCfg.Get(r.Context())
	if err != nil {
		h.logger.Error("admin: get lidarr config", "err", err)
		writeAdminJSONErr(w, http.StatusInternalServerError, "internal_error")
		return
	}
	writeJSON(w, http.StatusOK, configToView(cfg))
}

// putLidarrConfigBody is the decoded JSON body for PUT /api/admin/lidarr/config.
type putLidarrConfigBody struct {
	Enabled                  bool   `json:"enabled"`
	BaseURL                  string `json:"base_url"`
	APIKey                   string `json:"api_key"`
	DefaultQualityProfileID  int    `json:"default_quality_profile_id"`
	DefaultMetadataProfileID int    `json:"default_metadata_profile_id"`
	DefaultRootFolderPath    string `json:"default_root_folder_path"`
}

// handlePutLidarrConfig implements PUT /api/admin/lidarr/config.
// Empty api_key in the body preserves the currently saved api_key.
func (h *handlers) handlePutLidarrConfig(w http.ResponseWriter, r *http.Request) {
	var body putLidarrConfigBody
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		writeAdminJSONErr(w, http.StatusBadRequest, "bad_request")
		return
	}

	// Resolve the effective api_key: empty in body = preserve saved value.
	apiKey := body.APIKey
	if apiKey == "" {
		saved, err := h.lidarrCfg.Get(r.Context())
		if err != nil {
			h.logger.Error("admin: put lidarr config: load saved", "err", err)
			writeAdminJSONErr(w, http.StatusInternalServerError, "internal_error")
			return
		}
		apiKey = saved.APIKey
	}

	// Validate: enabled=true requires base_url AND api_key.
	if body.Enabled {
		if body.BaseURL == "" || apiKey == "" {
			writeAdminJSONErr(w, http.StatusBadRequest, "missing_required_field")
			return
		}
		// Validate that the URL parses.
		if _, err := url.ParseRequestURI(body.BaseURL); err != nil {
			writeAdminJSONErr(w, http.StatusBadRequest, "invalid_url")
			return
		}
		// Defaults must be present so future Approve calls don't dispatch
		// invalid POST bodies to Lidarr (which produces 4xx with field
		// errors). The frontend pre-selects first values so this almost
		// never fires for non-malicious traffic, but it's the right gate.
		if body.DefaultQualityProfileID == 0 ||
			body.DefaultMetadataProfileID == 0 ||
			body.DefaultRootFolderPath == "" {
			writeAdminJSONErr(w, http.StatusBadRequest, "missing_defaults")
			return
		}
	}

	cfg := lidarrconfig.Config{
		Enabled:                  body.Enabled,
		BaseURL:                  body.BaseURL,
		APIKey:                   apiKey,
		DefaultQualityProfileID:  body.DefaultQualityProfileID,
		DefaultMetadataProfileID: body.DefaultMetadataProfileID,
		DefaultRootFolderPath:    body.DefaultRootFolderPath,
	}
	if err := h.lidarrCfg.Save(r.Context(), cfg); err != nil {
		h.logger.Error("admin: put lidarr config: save", "err", err)
		writeAdminJSONErr(w, http.StatusInternalServerError, "internal_error")
		return
	}
	writeJSON(w, http.StatusOK, configToView(cfg))
}

// testLidarrBody is the optional JSON body for POST /api/admin/lidarr/test.
type testLidarrBody struct {
	BaseURL string `json:"base_url"`
	APIKey  string `json:"api_key"`
}

// testLidarrResponse is the JSON shape returned by POST /api/admin/lidarr/test.
// All fields except Ok are optional. On a failed connection (Ok=false), only
// Ok and Error are populated. On success (Ok=true), Version is populated;
// QualityProfiles / MetadataProfiles / RootFolders carry the live Lidarr
// data and ListErrors maps any per-list fetch failures by list name. The
// integrations page consumes the lists to pre-fill its dropdowns on
// first-time setup, fixing the chicken-and-egg where the dropdowns
// previously gated on cfg.Enabled.
type testLidarrResponse struct {
	Ok               bool                  `json:"ok"`
	Version          string                `json:"version,omitempty"`
	Error            string                `json:"error,omitempty"`
	QualityProfiles  []qualityProfileView  `json:"quality_profiles,omitempty"`
	MetadataProfiles []metadataProfileView `json:"metadata_profiles,omitempty"`
	RootFolders      []rootFolderView      `json:"root_folders,omitempty"`
	ListErrors       map[string]string     `json:"list_errors,omitempty"`
}

// handleTestLidarrConnection implements POST /api/admin/lidarr/test.
// Always returns 200; the ok/error fields in the response body indicate
// connection success or failure.
func (h *handlers) handleTestLidarrConnection(w http.ResponseWriter, r *http.Request) {
	var body testLidarrBody
	// Ignore decode errors — an empty body is valid (all fields optional).
	_ = json.NewDecoder(r.Body).Decode(&body)

	// Fall back to saved config for any empty field.
	baseURL := body.BaseURL
	apiKey := body.APIKey
	if baseURL == "" || apiKey == "" {
		saved, err := h.lidarrCfg.Get(r.Context())
		if err != nil {
			h.logger.Error("admin: test lidarr: load config", "err", err)
			writeJSON(w, http.StatusOK, map[string]any{"ok": false, "error": "internal_error"})
			return
		}
		if baseURL == "" {
			baseURL = saved.BaseURL
		}
		if apiKey == "" {
			apiKey = saved.APIKey
		}
	}

	client := lidarr.NewClient(baseURL, apiKey)
	result, err := client.Ping(r.Context())
	if err != nil {
		errCode := lidarrErrCode(err)
		// Warn (not Error): a failed connection test is expected when the
		// admin is configuring the integration and may have a typo'd URL
		// or wrong key. Without this log line the underlying *net.DNSError
		// / *net.OpError / TLS error is invisible to operators — they only
		// see the bucket code in the UI. base_url is safe to log; api_key
		// must NEVER be logged.
		h.logger.Warn("admin: test lidarr ping failed",
			"err", err, "base_url", baseURL, "code", errCode)
		writeJSON(w, http.StatusOK, testLidarrResponse{Ok: false, Error: errCode})
		return
	}
	quality, metadata, folders, listErrs := fetchLidarrLists(r.Context(), client)
	writeJSON(w, http.StatusOK, testLidarrResponse{
		Ok:               true,
		Version:          result.Version,
		QualityProfiles:  quality,
		MetadataProfiles: metadata,
		RootFolders:      folders,
		ListErrors:       listErrs,
	})
}

// fetchLidarrLists fans out the three list calls in parallel and returns
// the lists plus a map of any per-list errors. Empty map (returned as nil
// to omit the JSON field) if all succeeded. Used by the test handler to
// pre-populate the integrations page dropdowns on the same round-trip as
// the connection check.
func fetchLidarrLists(ctx context.Context, client *lidarr.Client) (
	[]qualityProfileView, []metadataProfileView, []rootFolderView, map[string]string,
) {
	var (
		quality  []qualityProfileView
		metadata []metadataProfileView
		folders  []rootFolderView
		errMu    sync.Mutex
		errs     = map[string]string{}
		wg       sync.WaitGroup
	)
	recordErr := func(name string, err error) {
		errMu.Lock()
		errs[name] = lidarrErrCode(err)
		errMu.Unlock()
	}

	wg.Add(3)
	go func() {
		defer wg.Done()
		ps, err := client.ListQualityProfiles(ctx)
		if err != nil {
			recordErr("quality_profiles", err)
			return
		}
		quality = make([]qualityProfileView, len(ps))
		for i, p := range ps {
			quality[i] = qualityProfileView{ID: p.ID, Name: p.Name}
		}
	}()
	go func() {
		defer wg.Done()
		ps, err := client.ListMetadataProfiles(ctx)
		if err != nil {
			recordErr("metadata_profiles", err)
			return
		}
		metadata = make([]metadataProfileView, len(ps))
		for i, p := range ps {
			metadata[i] = metadataProfileView{ID: p.ID, Name: p.Name}
		}
	}()
	go func() {
		defer wg.Done()
		fs, err := client.ListRootFolders(ctx)
		if err != nil {
			recordErr("root_folders", err)
			return
		}
		folders = make([]rootFolderView, len(fs))
		for i, f := range fs {
			folders[i] = rootFolderView{Path: f.Path, Accessible: f.Accessible, FreeSpace: f.FreeSpace}
		}
	}()
	wg.Wait()

	if len(errs) == 0 {
		return quality, metadata, folders, nil
	}
	return quality, metadata, folders, errs
}

// lidarrErrCode maps a lidarr client error to the stable string code used in
// admin API responses.
func lidarrErrCode(err error) string {
	switch {
	case errors.Is(err, lidarr.ErrUnreachable):
		return "lidarr_unreachable"
	case errors.Is(err, lidarr.ErrAuthFailed):
		return "lidarr_auth_failed"
	case errors.Is(err, lidarr.ErrLookupFailed):
		return "lidarr_lookup_failed"
	default:
		return "internal_error"
	}
}

// qualityProfileView is the JSON shape for a single quality profile in the
// GET /api/admin/lidarr/quality-profiles response.
type qualityProfileView struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

// handleListQualityProfiles implements GET /api/admin/lidarr/quality-profiles.
func (h *handlers) handleListQualityProfiles(w http.ResponseWriter, r *http.Request) {
	_, client, ok := h.lidarrClientFromConfig(w, r)
	if !ok {
		return
	}

	profiles, err := client.ListQualityProfiles(r.Context())
	if err != nil {
		writeAdminJSONErr(w, http.StatusServiceUnavailable, lidarrErrCode(err))
		return
	}
	out := make([]qualityProfileView, len(profiles))
	for i, p := range profiles {
		out[i] = qualityProfileView{ID: p.ID, Name: p.Name}
	}
	writeJSON(w, http.StatusOK, out)
}

// metadataProfileView is the JSON shape for a single metadata profile in the
// GET /api/admin/lidarr/metadata-profiles response.
type metadataProfileView struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

// handleListMetadataProfiles implements GET /api/admin/lidarr/metadata-profiles.
func (h *handlers) handleListMetadataProfiles(w http.ResponseWriter, r *http.Request) {
	_, client, ok := h.lidarrClientFromConfig(w, r)
	if !ok {
		return
	}

	profiles, err := client.ListMetadataProfiles(r.Context())
	if err != nil {
		writeAdminJSONErr(w, http.StatusServiceUnavailable, lidarrErrCode(err))
		return
	}
	out := make([]metadataProfileView, len(profiles))
	for i, p := range profiles {
		out[i] = metadataProfileView{ID: p.ID, Name: p.Name}
	}
	writeJSON(w, http.StatusOK, out)
}

// rootFolderView is the JSON shape for a single root folder in the
// GET /api/admin/lidarr/root-folders response.
type rootFolderView struct {
	Path       string `json:"path"`
	Accessible bool   `json:"accessible"`
	FreeSpace  int64  `json:"free_space"`
}

// handleListRootFolders implements GET /api/admin/lidarr/root-folders.
func (h *handlers) handleListRootFolders(w http.ResponseWriter, r *http.Request) {
	_, client, ok := h.lidarrClientFromConfig(w, r)
	if !ok {
		return
	}

	folders, err := client.ListRootFolders(r.Context())
	if err != nil {
		writeAdminJSONErr(w, http.StatusServiceUnavailable, lidarrErrCode(err))
		return
	}
	out := make([]rootFolderView, len(folders))
	for i, f := range folders {
		out[i] = rootFolderView{Path: f.Path, Accessible: f.Accessible, FreeSpace: f.FreeSpace}
	}
	writeJSON(w, http.StatusOK, out)
}

// lidarrClientFromConfig is a shared helper for handlers that need to proxy a
// request to Lidarr. It loads the saved config, returns 503 when disabled, and
// constructs a Client. Returns (cfg, client, true) on success; (_, nil, false)
// when a response has already been written.
func (h *handlers) lidarrClientFromConfig(w http.ResponseWriter, r *http.Request) (lidarrconfig.Config, *lidarr.Client, bool) {
	cfg, err := h.lidarrCfg.Get(r.Context())
	if err != nil {
		h.logger.Error("admin: lidarr proxy: load config", "err", err)
		writeAdminJSONErr(w, http.StatusInternalServerError, "internal_error")
		return lidarrconfig.Config{}, nil, false
	}
	if !cfg.Enabled {
		writeAdminJSONErr(w, http.StatusServiceUnavailable, "lidarr_disabled")
		return lidarrconfig.Config{}, nil, false
	}
	return cfg, lidarr.NewClient(cfg.BaseURL, cfg.APIKey), true
}

// writeAdminJSONErr writes a flat {"error":"<code>"} JSON response. This
// mirrors the shape the RequireAdmin middleware uses and that the spec
// defines for /api/admin/* errors.
func writeAdminJSONErr(w http.ResponseWriter, status int, code string) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(map[string]string{"error": code})
}
