package exec

import (
	"context"
	"errors"
	"fmt"
	"io"
	"mime"
	"mime/multipart"
	"net/textproto"
	"net/url"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/tidbcloud/AgentTeam/desktop/internal/wire/apiclient"
	"github.com/tidbcloud/AgentTeam/desktop/internal/wire/workspacefs"
)

const maxSkillImportBytes int64 = 64 << 20

type skillRemote struct {
	client  *apiclient.Client
	workDir string
	read    remoteReadTransport
	natural naturalMutationTransport
	receipt requestReceiptTransport

	beforeSecureOpen func(string)
}

func (remote skillRemote) readTransport() remoteReadTransport {
	transport := remote.read
	transport.client = remote.client
	return transport
}

func (remote skillRemote) naturalTransport() naturalMutationTransport {
	transport := remote.natural
	transport.client = remote.client
	return transport
}

func (remote skillRemote) receiptTransport() requestReceiptTransport {
	transport := remote.receipt
	transport.client = remote.client
	return transport
}

func (remote skillRemote) list(ctx context.Context, input skillListRequest) (skillListResult, error) {
	if err := requireRuntimeClient(remote.client); err != nil {
		return skillListResult{}, err
	}
	values := url.Values{}
	if value := strings.TrimSpace(input.Query); value != "" {
		values.Set("q", value)
	}
	if input.Page != 0 {
		values.Set("page", strconv.Itoa(input.Page))
	}
	if input.PerPage != 0 {
		values.Set("per_page", strconv.Itoa(input.PerPage))
	}
	if value := strings.TrimSpace(input.TemplateLocale); value != "" {
		values.Set("template_locale", value)
	}
	if value := strings.TrimSpace(input.View); value != "" {
		values.Set("view", value)
	}
	path := remote.client.AgentRuntimePath("/skills")
	if encoded := values.Encode(); encoded != "" {
		path += "?" + encoded
	}
	var result skillListResult
	err := remote.readTransport().get(ctx, path, &result)
	return result, err
}

func (remote skillRemote) get(ctx context.Context, input skillGetRequest) (skill, error) {
	if err := requireRuntimeClient(remote.client); err != nil {
		return skill{}, err
	}
	var result skill
	err := remote.readTransport().post(
		ctx, remote.client.AgentRuntimePath("/skills/get"), input, &result,
	)
	return result, err
}

func (remote skillRemote) create(ctx context.Context, input skillCreateRequest) (skill, error) {
	if err := requireRuntimeClient(remote.client); err != nil {
		return skill{}, err
	}
	var result skill
	err := remote.naturalTransport().post(
		ctx, remote.client.AgentRuntimePath("/skills/create"), input, &result,
	)
	return result, err
}

func (remote skillRemote) update(ctx context.Context, input skillUpdateRequest) (skill, error) {
	if err := requireRuntimeClient(remote.client); err != nil {
		return skill{}, err
	}
	var result skill
	err := remote.naturalTransport().post(
		ctx, remote.client.AgentRuntimePath("/skills/update"), input, &result,
	)
	return result, err
}

func (remote skillRemote) delete(
	ctx context.Context,
	input skillDeleteRequest,
) (skillDeleteResult, error) {
	if err := requireRuntimeClient(remote.client); err != nil {
		return skillDeleteResult{}, err
	}
	var result skillDeleteResult
	err := remote.receiptTransport().post(
		ctx, remote.client.AgentRuntimePath("/skills/delete"), input, &result, replayHeaders(input.RequestID),
	)
	return result, err
}

func (remote skillRemote) importContent(ctx context.Context, input skillImportRequest) (skillImportResult, error) {
	if err := requireRuntimeClient(remote.client); err != nil {
		return skillImportResult{}, err
	}
	skillID := strings.TrimSpace(input.SkillID)
	filePath := strings.TrimSpace(input.File)
	sourceURL := strings.TrimSpace(input.URL)
	if skillID == "" || (filePath == "") == (sourceURL == "") {
		return skillImportResult{}, NewError(CodeValidation, "skill import requires skillId and exactly one file or url")
	}
	path := remote.client.AgentRuntimePath("/skills/import") + "?" +
		url.Values{"skillId": []string{skillID}}.Encode()

	if sourceURL != "" {
		var result skillImportResult
		err := remote.naturalTransport().multipartStream(ctx, path, func(writer *multipart.Writer) error {
			return writer.WriteField("url", sourceURL)
		}, &result)
		return result, err
	}

	file, filename, err := remote.openImportFile(filePath)
	if err != nil {
		return skillImportResult{}, classifySkillImportFileError(err)
	}
	defer file.Close()

	var result skillImportResult
	err = remote.naturalTransport().multipartStream(ctx, path, func(writer *multipart.Writer) error {
		if _, seekErr := file.Seek(0, io.SeekStart); seekErr != nil {
			return fmt.Errorf("rewind Skill import file: %w", seekErr)
		}
		part, createErr := writer.CreatePart(textproto.MIMEHeader{
			"Content-Disposition": {
				mime.FormatMediaType("form-data", map[string]string{
					"name": "file", "filename": filename,
				}),
			},
			"Content-Type": {"application/octet-stream"},
		})
		if createErr != nil {
			return fmt.Errorf("create Skill import file part: %w", createErr)
		}
		written, copyErr := io.Copy(part, io.LimitReader(file, maxSkillImportBytes+1))
		if copyErr != nil {
			return fmt.Errorf("stream Skill import file: %w", copyErr)
		}
		if written > maxSkillImportBytes {
			return errSecureFileTooLarge
		}
		return nil
	}, &result)
	if err != nil {
		if errors.Is(err, errSecureFileTooLarge) {
			return skillImportResult{}, classifySkillImportFileError(errSecureFileTooLarge)
		}
		return skillImportResult{}, err
	}
	return result, nil
}

func (remote skillRemote) openImportFile(rawPath string) (io.ReadSeekCloser, string, error) {
	if strings.TrimSpace(remote.workDir) == "" || filepath.IsAbs(rawPath) {
		return nil, "", errSecureFileRejected
	}
	absBase, err := filepath.Abs(remote.workDir)
	if err != nil {
		return nil, "", fmt.Errorf("resolve Skill import workdir: %w", err)
	}
	resolved, err := workspacefs.ResolvePathWithinBase(absBase, rawPath)
	if err != nil {
		return nil, "", errSecureFileRejected
	}
	if remote.beforeSecureOpen != nil {
		remote.beforeSecureOpen(resolved)
	}
	file, err := secureOpenRegularFile(absBase, resolved, maxSkillImportBytes)
	if err != nil {
		return nil, "", err
	}
	return file, filepath.Base(resolved), nil
}

func classifySkillImportFileError(err error) *Error {
	switch {
	case errors.Is(err, errSecureFileNotFound):
		return &Error{Code: CodeNotFound, Problem: "Skill import file was not found"}
	case errors.Is(err, errSecureFileTooLarge):
		return &Error{Code: CodeValidation, Problem: "Skill import file exceeds 64 MiB"}
	case errors.Is(err, errSecureFileRejected):
		return &Error{
			Code:    CodePermissionDenied,
			Problem: "Skill import file must be a relative regular file inside the session workdir without symlinks",
		}
	default:
		return &Error{Code: CodeInternal, Problem: "Could not open Skill import file", Cause: err}
	}
}
