package httpserver

import (
	"errors"
	"fmt"
	"mime"
	"net/http"
	"strings"
	"time"

	"github.com/go-chi/chi/v5"

	"update_server/internal/apikeys"
	"update_server/internal/db"
)

var errInvalidReleaseID = errors.New("invalid release id")

type apiProjectResponse struct {
	ID               int64  `json:"id"`
	Name             string `json:"name"`
	Slug             string `json:"slug"`
	Description      string `json:"description"`
	LatestReleaseURL string `json:"latest_release_url"`
}

type apiReleaseResponse struct {
	ID             int64     `json:"id"`
	Version        string    `json:"version"`
	Build          string    `json:"build"`
	Filename       string    `json:"filename"`
	ChecksumSHA256 string    `json:"checksum_sha256"`
	SizeBytes      int64     `json:"size_bytes"`
	ContentType    string    `json:"content_type"`
	ReleaseNotes   string    `json:"release_notes"`
	CreatedAt      time.Time `json:"created_at"`
	MetadataURL    string    `json:"metadata_url"`
	DownloadURL    string    `json:"download_url"`
}

func (h *handler) apiAccessibleProjects(w http.ResponseWriter, r *http.Request) {
	state, ok := apikeys.FromContext(r.Context())
	if !ok {
		writeAPIKeyAuthError(w, http.StatusUnauthorized, "api key authentication required")
		return
	}

	projects, err := h.apiKeys.ListAccessibleProjects(r.Context(), state.APIKey)
	if err != nil {
		writeJSON(w, http.StatusInternalServerError, map[string]any{"error": "project listing failed"})
		return
	}

	items := make([]apiProjectResponse, 0, len(projects))
	for _, project := range projects {
		items = append(items, apiProjectPayload(project))
	}

	writeJSON(w, http.StatusOK, map[string]any{"projects": items})
}

func (h *handler) apiLatestRelease(w http.ResponseWriter, r *http.Request) {
	state, ok := apikeys.FromContext(r.Context())
	if !ok {
		writeAPIKeyAuthError(w, http.StatusUnauthorized, "api key authentication required")
		return
	}

	project, err := h.accessibleProjectBySlug(r, state.APIKey)
	if err != nil {
		h.writeClientResourceError(w, err, "project lookup failed")
		return
	}

	release, err := h.store.Releases.GetLatestByProjectID(r.Context(), project.ID)
	if err != nil {
		if errors.Is(err, db.ErrNotFound) {
			writeJSON(w, http.StatusNotFound, map[string]any{"error": "release not found"})
			return
		}

		writeJSON(w, http.StatusInternalServerError, map[string]any{"error": "release lookup failed"})
		return
	}

	writeJSON(w, http.StatusOK, map[string]any{
		"project": apiProjectPayload(*project),
		"release": apiReleasePayload(*release),
	})
}

func (h *handler) apiReleaseMetadata(w http.ResponseWriter, r *http.Request) {
	state, ok := apikeys.FromContext(r.Context())
	if !ok {
		writeAPIKeyAuthError(w, http.StatusUnauthorized, "api key authentication required")
		return
	}

	project, release, err := h.accessibleReleaseByID(r, state.APIKey)
	if err != nil {
		h.writeClientResourceError(w, err, "release lookup failed")
		return
	}

	writeJSON(w, http.StatusOK, map[string]any{
		"project": apiProjectPayload(*project),
		"release": apiReleasePayload(*release),
	})
}

func (h *handler) apiReleaseDownload(w http.ResponseWriter, r *http.Request) {
	state, ok := apikeys.FromContext(r.Context())
	if !ok {
		writeAPIKeyAuthError(w, http.StatusUnauthorized, "api key authentication required")
		return
	}

	_, release, err := h.accessibleReleaseByID(r, state.APIKey)
	if err != nil {
		h.writeClientResourceError(w, err, "release lookup failed")
		return
	}

	file, err := h.releases.Artifact(release.StoragePath)
	if err != nil {
		if errors.Is(err, db.ErrNotFound) {
			writeJSON(w, http.StatusNotFound, map[string]any{"error": "release not found"})
			return
		}

		h.logger.Error("artifact open failed", "release_id", release.ID, "storage_path", release.StoragePath, "error", err)
		writeJSON(w, http.StatusInternalServerError, map[string]any{"error": "artifact download failed"})
		return
	}
	defer file.Close()

	disposition := mime.FormatMediaType("attachment", map[string]string{"filename": release.Filename})
	if disposition != "" {
		w.Header().Set("Content-Disposition", disposition)
	}
	w.Header().Set("Content-Type", release.ContentType)
	w.Header().Set("X-Content-Type-Options", "nosniff")

	http.ServeContent(w, r, release.Filename, release.UpdatedAt, file)
}

func (h *handler) accessibleProjectBySlug(r *http.Request, apiKey db.APIKey) (*db.Project, error) {
	projectSlug := strings.TrimSpace(chi.URLParam(r, "projectSlug"))
	if projectSlug == "" {
		return nil, db.ErrNotFound
	}

	project, err := h.store.Projects.GetBySlug(r.Context(), projectSlug)
	if err != nil {
		return nil, err
	}

	if !project.IsActive {
		return nil, db.ErrNotFound
	}

	allowed, err := h.apiKeys.CanAccessProject(r.Context(), apiKey, project.ID)
	if err != nil {
		return nil, fmt.Errorf("check project access: %w", err)
	}

	if !allowed {
		return nil, db.ErrNotFound
	}

	return project, nil
}

func (h *handler) accessibleReleaseByID(r *http.Request, apiKey db.APIKey) (*db.Project, *db.Release, error) {
	releaseID, err := routeID(r, "releaseID")
	if err != nil {
		return nil, nil, errInvalidReleaseID
	}

	release, err := h.store.Releases.GetByID(r.Context(), releaseID)
	if err != nil {
		return nil, nil, err
	}

	if !release.IsActive {
		return nil, nil, db.ErrNotFound
	}

	project, err := h.store.Projects.GetByID(r.Context(), release.ProjectID)
	if err != nil {
		return nil, nil, err
	}

	if !project.IsActive {
		return nil, nil, db.ErrNotFound
	}

	allowed, err := h.apiKeys.CanAccessProject(r.Context(), apiKey, project.ID)
	if err != nil {
		return nil, nil, fmt.Errorf("check release project access: %w", err)
	}

	if !allowed {
		return nil, nil, db.ErrNotFound
	}

	return project, release, nil
}

func (h *handler) writeClientResourceError(w http.ResponseWriter, err error, message string) {
	switch {
	case err == nil:
		return
	case errors.Is(err, db.ErrNotFound):
		writeJSON(w, http.StatusNotFound, map[string]any{"error": "resource not found"})
	case errors.Is(err, errInvalidReleaseID):
		writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid release id"})
	default:
		writeJSON(w, http.StatusInternalServerError, map[string]any{"error": message})
	}
}

func apiProjectPayload(project db.Project) apiProjectResponse {
	return apiProjectResponse{
		ID:               project.ID,
		Name:             project.Name,
		Slug:             project.Slug,
		Description:      project.Description,
		LatestReleaseURL: fmt.Sprintf("/api/v1/projects/%s/releases/latest", project.Slug),
	}
}

func apiReleasePayload(release db.Release) apiReleaseResponse {
	return apiReleaseResponse{
		ID:             release.ID,
		Version:        release.Version,
		Build:          release.Build,
		Filename:       release.Filename,
		ChecksumSHA256: release.ChecksumSHA256,
		SizeBytes:      release.SizeBytes,
		ContentType:    release.ContentType,
		ReleaseNotes:   release.ReleaseNotes,
		CreatedAt:      release.CreatedAt,
		MetadataURL:    fmt.Sprintf("/api/v1/releases/%d", release.ID),
		DownloadURL:    fmt.Sprintf("/api/v1/releases/%d/download", release.ID),
	}
}
