// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package repo

import (
	"net/http"
	"strconv"

	repo_model "code.gitea.io/gitea/models/repo"
	"code.gitea.io/gitea/modules/log"
	"code.gitea.io/gitea/modules/setting"
	api "code.gitea.io/gitea/modules/structs"
	attachment_service "code.gitea.io/gitea/services/attachment"
	"code.gitea.io/gitea/services/context"
	"code.gitea.io/gitea/services/convert"
)

// CreateUploadSession creates a new chunked upload session
func CreateUploadSession(ctx *context.APIContext) {
	// swagger:operation POST /repos/{owner}/{repo}/releases/{id}/assets/upload-session repository repoCreateUploadSession
	// ---
	// summary: Create a chunked upload session for a release attachment
	// description: Creates a new upload session that allows uploading large files in chunks. Use this for files larger than 100MB or when network reliability is a concern.
	// produces:
	// - application/json
	// consumes:
	// - application/json
	// parameters:
	// - name: owner
	//   in: path
	//   description: owner of the repo
	//   type: string
	//   required: true
	// - name: repo
	//   in: path
	//   description: name of the repo
	//   type: string
	//   required: true
	// - name: id
	//   in: path
	//   description: id of the release
	//   type: integer
	//   format: int64
	//   required: true
	// - name: name
	//   in: query
	//   description: name of the attachment
	//   type: string
	//   required: true
	// - name: size
	//   in: query
	//   description: total file size in bytes (optional but recommended for validation)
	//   type: integer
	//   format: int64
	//   required: false
	// - name: chunk_size
	//   in: query
	//   description: chunk size in bytes (default 10MB, max 100MB)
	//   type: integer
	//   format: int64
	//   required: false
	// responses:
	//   "201":
	//     "$ref": "#/responses/UploadSession"
	//   "400":
	//     "$ref": "#/responses/error"
	//   "404":
	//     "$ref": "#/responses/notFound"

	if !setting.Attachment.Enabled {
		ctx.APIErrorNotFound("Attachment is not enabled")
		return
	}

	releaseID := ctx.PathParamInt64("id")
	if !checkReleaseMatchRepo(ctx, releaseID) {
		return
	}

	filename := ctx.FormString("name")
	if filename == "" {
		ctx.APIError(http.StatusBadRequest, "name is required")
		return
	}

	fileSize := ctx.FormInt64("size")
	if fileSize == 0 {
		fileSize = -1 // Unknown size
	}

	chunkSize := ctx.FormInt64("chunk_size")
	if chunkSize <= 0 {
		chunkSize = repo_model.DefaultChunkSize
	}
	// Cap chunk size at 100MB
	if chunkSize > 100*1024*1024 {
		chunkSize = 100 * 1024 * 1024
	}

	session, err := attachment_service.CreateChunkedUploadSession(ctx, attachment_service.ChunkedUploadOptions{
		RepoID:     ctx.Repo.Repository.ID,
		ReleaseID:  releaseID,
		UploaderID: ctx.Doer.ID,
		FileName:   filename,
		FileSize:   fileSize,
		ChunkSize:  chunkSize,
	})
	if err != nil {
		ctx.APIErrorInternal(err)
		return
	}

	ctx.JSON(http.StatusCreated, api.UploadSessionResponse{
		UUID:           session.UUID,
		FileName:       session.FileName,
		FileSize:       session.FileSize,
		ChunkSize:      session.ChunkSize,
		ChunksExpected: session.ChunksExpected,
		ExpiresAt:      int64(session.ExpiresUnix),
	})
}

// UploadChunk uploads a chunk to an upload session
func UploadChunk(ctx *context.APIContext) {
	// swagger:operation PUT /repos/{owner}/{repo}/uploads/{session_id}/chunks/{chunk_number} repository repoUploadChunk
	// ---
	// summary: Upload a chunk to an upload session
	// description: |
	//   Uploads a single chunk of data to an existing upload session. Chunks can be uploaded in any order.
	//   Optionally include X-Chunk-Checksum header with SHA-256 hash for data integrity verification.
	// produces:
	// - application/json
	// consumes:
	// - application/octet-stream
	// parameters:
	// - name: owner
	//   in: path
	//   description: owner of the repo
	//   type: string
	//   required: true
	// - name: repo
	//   in: path
	//   description: name of the repo
	//   type: string
	//   required: true
	// - name: session_id
	//   in: path
	//   description: upload session UUID
	//   type: string
	//   required: true
	// - name: chunk_number
	//   in: path
	//   description: chunk number (0-indexed)
	//   type: integer
	//   format: int64
	//   required: true
	// - name: X-Chunk-Checksum
	//   in: header
	//   description: SHA-256 checksum of the chunk data (hex-encoded, optional)
	//   type: string
	//   required: false
	// - name: body
	//   in: body
	//   description: chunk data
	//   required: true
	// responses:
	//   "200":
	//     "$ref": "#/responses/UploadChunkResponse"
	//   "400":
	//     "$ref": "#/responses/error"
	//   "404":
	//     "$ref": "#/responses/notFound"
	//   "410":
	//     "$ref": "#/responses/error"

	sessionUUID := ctx.PathParam("session_id")
	chunkNumber, err := strconv.ParseInt(ctx.PathParam("chunk_number"), 10, 64)
	if err != nil {
		ctx.APIError(http.StatusBadRequest, "invalid chunk number")
		return
	}

	session, err := repo_model.GetUploadSessionByUUID(ctx, sessionUUID)
	if err != nil {
		if repo_model.IsErrUploadSessionNotExist(err) {
			ctx.APIErrorNotFound()
			return
		}
		ctx.APIErrorInternal(err)
		return
	}

	// Verify session belongs to this repo
	if session.RepoID != ctx.Repo.Repository.ID {
		ctx.APIErrorNotFound()
		return
	}

	// Verify uploader
	if session.UploaderID != ctx.Doer.ID {
		ctx.APIError(http.StatusForbidden, "not the session owner")
		return
	}

	// Check if session is expired
	if session.IsExpired() {
		ctx.APIError(http.StatusGone, "upload session has expired")
		return
	}

	// Get Content-Length for size validation
	contentLength := ctx.Req.ContentLength

	// Get optional checksum header for integrity verification
	checksum := ctx.Req.Header.Get("X-Chunk-Checksum")

	err = attachment_service.SaveChunkWithOptions(ctx, session, attachment_service.ChunkSaveOptions{
		ChunkNumber: chunkNumber,
		Data:        ctx.Req.Body,
		Size:        contentLength,
		Checksum:    checksum,
	})
	if err != nil {
		if repo_model.IsErrUploadSessionExpired(err) {
			ctx.APIError(http.StatusGone, err.Error())
			return
		}
		if attachment_service.IsErrChecksumMismatch(err) {
			ctx.APIError(http.StatusBadRequest, err.Error())
			return
		}
		ctx.APIError(http.StatusBadRequest, err.Error())
		return
	}

	// Refresh session from DB
	session, _ = repo_model.GetUploadSessionByUUID(ctx, sessionUUID)

	ctx.JSON(http.StatusOK, api.UploadChunkResponse{
		ChunkNumber:    chunkNumber,
		ChunksReceived: session.ChunksReceived,
		BytesReceived:  session.BytesReceived,
		Complete:       session.IsComplete(),
	})
}

// GetUploadSession returns information about an upload session
func GetUploadSession(ctx *context.APIContext) {
	// swagger:operation GET /repos/{owner}/{repo}/uploads/{session_id} repository repoGetUploadSession
	// ---
	// summary: Get upload session status
	// description: Returns the current status of an upload session, including which chunks have been received. Use this to resume interrupted uploads.
	// produces:
	// - application/json
	// parameters:
	// - name: owner
	//   in: path
	//   description: owner of the repo
	//   type: string
	//   required: true
	// - name: repo
	//   in: path
	//   description: name of the repo
	//   type: string
	//   required: true
	// - name: session_id
	//   in: path
	//   description: upload session UUID
	//   type: string
	//   required: true
	// responses:
	//   "200":
	//     "$ref": "#/responses/UploadSessionInfo"
	//   "404":
	//     "$ref": "#/responses/notFound"

	sessionUUID := ctx.PathParam("session_id")

	session, err := repo_model.GetUploadSessionByUUID(ctx, sessionUUID)
	if err != nil {
		if repo_model.IsErrUploadSessionNotExist(err) {
			ctx.APIErrorNotFound()
			return
		}
		ctx.APIErrorInternal(err)
		return
	}

	// Verify session belongs to this repo
	if session.RepoID != ctx.Repo.Repository.ID {
		ctx.APIErrorNotFound()
		return
	}

	chunks, err := attachment_service.GetReceivedChunks(session)
	if err != nil {
		ctx.APIErrorInternal(err)
		return
	}

	statusStr := "active"
	switch session.Status {
	case repo_model.UploadSessionStatusComplete:
		statusStr = "complete"
	case repo_model.UploadSessionStatusExpired:
		statusStr = "expired"
	case repo_model.UploadSessionStatusFailed:
		statusStr = "failed"
	}

	ctx.JSON(http.StatusOK, api.UploadSessionInfo{
		UUID:           session.UUID,
		FileName:       session.FileName,
		FileSize:       session.FileSize,
		ChunkSize:      session.ChunkSize,
		ChunksExpected: session.ChunksExpected,
		ChunksReceived: session.ChunksReceived,
		BytesReceived:  session.BytesReceived,
		ReceivedChunks: chunks,
		Status:         statusStr,
		ExpiresAt:      int64(session.ExpiresUnix),
	})
}

// CompleteUploadSession finalizes an upload session and creates the attachment
func CompleteUploadSession(ctx *context.APIContext) {
	// swagger:operation POST /repos/{owner}/{repo}/uploads/{session_id}/complete repository repoCompleteUploadSession
	// ---
	// summary: Complete an upload session
	// description: Assembles all uploaded chunks into the final file and creates the attachment. Call this after all chunks have been uploaded.
	// produces:
	// - application/json
	// parameters:
	// - name: owner
	//   in: path
	//   description: owner of the repo
	//   type: string
	//   required: true
	// - name: repo
	//   in: path
	//   description: name of the repo
	//   type: string
	//   required: true
	// - name: session_id
	//   in: path
	//   description: upload session UUID
	//   type: string
	//   required: true
	// responses:
	//   "201":
	//     "$ref": "#/responses/Attachment"
	//   "400":
	//     "$ref": "#/responses/error"
	//   "404":
	//     "$ref": "#/responses/notFound"

	sessionUUID := ctx.PathParam("session_id")

	session, err := repo_model.GetUploadSessionByUUID(ctx, sessionUUID)
	if err != nil {
		if repo_model.IsErrUploadSessionNotExist(err) {
			ctx.APIErrorNotFound()
			return
		}
		ctx.APIErrorInternal(err)
		return
	}

	// Verify session belongs to this repo
	if session.RepoID != ctx.Repo.Repository.ID {
		ctx.APIErrorNotFound()
		return
	}

	// Verify uploader
	if session.UploaderID != ctx.Doer.ID {
		ctx.APIError(http.StatusForbidden, "not the session owner")
		return
	}

	// Check if session is expired
	if session.IsExpired() {
		ctx.APIError(http.StatusGone, "upload session has expired")
		return
	}

	// Assemble chunks into final attachment
	attach, err := attachment_service.AssembleChunks(ctx, session, setting.Repository.Release.AllowedTypes)
	if err != nil {
		log.Error("Failed to assemble chunks for session %s: %v", sessionUUID, err)
		ctx.APIError(http.StatusBadRequest, err.Error())
		return
	}

	ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attach))
}

// CancelUploadSession cancels an upload session
func CancelUploadSession(ctx *context.APIContext) {
	// swagger:operation DELETE /repos/{owner}/{repo}/uploads/{session_id} repository repoCancelUploadSession
	// ---
	// summary: Cancel an upload session
	// description: Cancels an upload session and deletes any uploaded chunks.
	// produces:
	// - application/json
	// parameters:
	// - name: owner
	//   in: path
	//   description: owner of the repo
	//   type: string
	//   required: true
	// - name: repo
	//   in: path
	//   description: name of the repo
	//   type: string
	//   required: true
	// - name: session_id
	//   in: path
	//   description: upload session UUID
	//   type: string
	//   required: true
	// responses:
	//   "204":
	//     "$ref": "#/responses/empty"
	//   "404":
	//     "$ref": "#/responses/notFound"

	sessionUUID := ctx.PathParam("session_id")

	session, err := repo_model.GetUploadSessionByUUID(ctx, sessionUUID)
	if err != nil {
		if repo_model.IsErrUploadSessionNotExist(err) {
			ctx.APIErrorNotFound()
			return
		}
		ctx.APIErrorInternal(err)
		return
	}

	// Verify session belongs to this repo
	if session.RepoID != ctx.Repo.Repository.ID {
		ctx.APIErrorNotFound()
		return
	}

	// Verify uploader or repo admin
	if session.UploaderID != ctx.Doer.ID && !ctx.Repo.IsAdmin() {
		ctx.APIError(http.StatusForbidden, "not the session owner")
		return
	}

	if err := attachment_service.CancelUploadSession(ctx, session); err != nil {
		ctx.APIErrorInternal(err)
		return
	}

	ctx.Status(http.StatusNoContent)
}
