package releases

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"mime"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"unicode"

	"update_server/internal/db"
	"update_server/internal/storage"
)

type Service struct {
	store     *db.Store
	artifacts *storage.LocalStore
}

type UploadParams struct {
	ProjectID        int64
	Version          string
	Build            string
	ReleaseNotes     string
	OriginalFilename string
	DeclaredType     string
	Reader           io.Reader
	UploadedByUserID *int64
}

type UploadResult struct {
	Project *db.Project
	Release *db.Release
}

func NewService(store *db.Store, artifacts *storage.LocalStore) *Service {
	return &Service{
		store:     store,
		artifacts: artifacts,
	}
}

func (s *Service) Artifact(storagePath string) (*os.File, error) {
	file, err := s.artifacts.Open(storagePath)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return nil, db.ErrNotFound
		}

		return nil, fmt.Errorf("open artifact: %w", err)
	}

	return file, nil
}

func (s *Service) Upload(ctx context.Context, params UploadParams) (*UploadResult, error) {
	project, err := s.store.Projects.GetByID(ctx, params.ProjectID)
	if err != nil {
		return nil, fmt.Errorf("load upload project: %w", err)
	}

	version := strings.TrimSpace(params.Version)
	if version == "" {
		return nil, fmt.Errorf("release version is required")
	}

	sanitizedFilename := sanitizeFilename(params.OriginalFilename)
	if sanitizedFilename == "" {
		return nil, fmt.Errorf("uploaded file name is invalid")
	}

	tempFile, tempPath, err := s.artifacts.CreateTemp("upload-*")
	if err != nil {
		return nil, fmt.Errorf("create temp artifact: %w", err)
	}

	removeTemp := func() {
		if tempFile != nil {
			_ = tempFile.Close()
		}
		_ = s.artifacts.RemoveTemp(tempPath)
	}

	hash := sha256.New()
	multiWriter := io.MultiWriter(tempFile, hash)

	sniffBuffer := make([]byte, 0, 512)
	copyBuffer := make([]byte, 32*1024)
	var sizeBytes int64

	for {
		n, readErr := params.Reader.Read(copyBuffer)
		if n > 0 {
			chunk := copyBuffer[:n]
			if len(sniffBuffer) < 512 {
				remaining := 512 - len(sniffBuffer)
				if remaining > n {
					remaining = n
				}
				sniffBuffer = append(sniffBuffer, chunk[:remaining]...)
			}

			written, writeErr := multiWriter.Write(chunk)
			sizeBytes += int64(written)
			if writeErr != nil {
				removeTemp()
				return nil, fmt.Errorf("write temp artifact: %w", writeErr)
			}
		}

		if errors.Is(readErr, io.EOF) {
			break
		}

		if readErr != nil {
			removeTemp()
			return nil, fmt.Errorf("read upload stream: %w", readErr)
		}
	}

	if sizeBytes == 0 {
		removeTemp()
		return nil, fmt.Errorf("uploaded artifact is empty")
	}

	if err := tempFile.Close(); err != nil {
		removeTemp()
		return nil, fmt.Errorf("close temp artifact: %w", err)
	}
	tempFile = nil

	contentType := detectContentType(sniffBuffer, params.DeclaredType)
	storagePath := buildStoragePath(project.Slug, version, params.Build, sanitizedFilename)

	if err := s.artifacts.CommitTemp(tempPath, storagePath); err != nil {
		removeTemp()
		return nil, fmt.Errorf("store artifact: %w", err)
	}

	cleanupFinal := func() {
		_ = s.artifacts.Remove(storagePath)
	}

	release, err := s.store.Releases.Create(ctx, db.CreateReleaseParams{
		ProjectID:        project.ID,
		Version:          version,
		Build:            strings.TrimSpace(params.Build),
		Filename:         sanitizedFilename,
		StoragePath:      storagePath,
		ChecksumSHA256:   hex.EncodeToString(hash.Sum(nil)),
		SizeBytes:        sizeBytes,
		ContentType:      contentType,
		ReleaseNotes:     strings.TrimSpace(params.ReleaseNotes),
		UploadedByUserID: params.UploadedByUserID,
		IsActive:         true,
	})
	if err != nil {
		cleanupFinal()
		return nil, fmt.Errorf("create release metadata: %w", err)
	}

	return &UploadResult{
		Project: project,
		Release: release,
	}, nil
}

func sanitizeFilename(raw string) string {
	filename := filepath.Base(strings.ReplaceAll(strings.TrimSpace(raw), "\\", "/"))
	if filename == "." || filename == "" {
		return ""
	}

	ext := filepath.Ext(filename)
	name := strings.TrimSuffix(filename, ext)
	name = sanitizePathSegment(name)
	if name == "" {
		name = "artifact"
	}

	ext = sanitizeExtension(ext)
	return strings.Trim(name+ext, ".")
}

func sanitizeExtension(ext string) string {
	if ext == "" {
		return ""
	}

	ext = strings.ToLower(ext)
	var builder strings.Builder
	for _, r := range ext {
		switch {
		case r == '.':
			builder.WriteRune(r)
		case unicode.IsLetter(r), unicode.IsDigit(r):
			builder.WriteRune(r)
		}
	}

	if builder.Len() <= 1 {
		return ""
	}

	return builder.String()
}

func sanitizePathSegment(raw string) string {
	raw = strings.TrimSpace(strings.ToLower(raw))
	var builder strings.Builder
	lastDash := false

	for _, r := range raw {
		switch {
		case unicode.IsLetter(r), unicode.IsDigit(r):
			builder.WriteRune(r)
			lastDash = false
		case r == '.', r == '_', r == '-':
			builder.WriteRune(r)
			lastDash = false
		default:
			if !lastDash && builder.Len() > 0 {
				builder.WriteRune('-')
				lastDash = true
			}
		}
	}

	value := strings.Trim(builder.String(), "-._")
	if len(value) > 160 {
		value = strings.Trim(value[:160], "-._")
	}

	return value
}

func buildStoragePath(projectSlug, version, build, filename string) string {
	versionSegment := sanitizePathSegment(version)
	if versionSegment == "" {
		versionSegment = "release"
	}

	parts := []string{sanitizePathSegment(projectSlug), versionSegment}
	if build = sanitizePathSegment(build); build != "" {
		parts = append(parts, build)
	}
	parts = append(parts, filename)

	return filepath.ToSlash(filepath.Join(parts...))
}

func detectContentType(sniff []byte, declared string) string {
	if detected := http.DetectContentType(sniff); detected != "" && detected != "application/octet-stream" {
		return detected
	}

	if declared = strings.TrimSpace(declared); declared != "" {
		if mediaType, _, err := mime.ParseMediaType(declared); err == nil && mediaType != "" {
			return mediaType
		}
	}

	return "application/octet-stream"
}
