package skills

import (
	"archive/tar"
	"archive/zip"
	"compress/gzip"
	"context"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"strings"
	"time"
)

const (
	MaxSkillImportBytes int64 = 64 << 20

	maxSkillArchiveEntries              = 4096
	maxSkillArchiveEntryBytes     int64 = 16 << 20
	maxSkillArchiveExtractedBytes int64 = 64 << 20
)

type skillArchiveLimits struct {
	maxEntries        int
	maxEntryBytes     int64
	maxExtractedBytes int64
}

var defaultSkillArchiveLimits = skillArchiveLimits{
	maxEntries:        maxSkillArchiveEntries,
	maxEntryBytes:     maxSkillArchiveEntryBytes,
	maxExtractedBytes: maxSkillArchiveExtractedBytes,
}

func importSourceArchive(ctx context.Context, request SkillRepositoryImportRequest, workDir string) (string, func(), string, []SkillImportWarning, error) {
	sourcePath := strings.TrimSpace(request.SourceFilePath)
	sourceName := strings.TrimSpace(request.SourceName)
	cleanup := func() {}
	if sourcePath == "" {
		if strings.TrimSpace(request.SourceURL) == "" {
			return "", cleanup, "", nil, codedInvalidArgument(SkillImportErrorInvalidSource, "file or url is required")
		}
		var err error
		sourcePath, sourceName, cleanup, err = downloadSkillImportURL(ctx, request.SourceURL, workDir)
		if err != nil {
			return "", cleanup, "", nil, err
		}
	} else if sourceName == "" {
		sourceName = filepath.Base(sourcePath)
	}
	extractDir := filepath.Join(workDir, "source")
	if err := os.MkdirAll(extractDir, 0o755); err != nil {
		cleanup()
		return "", cleanup, "", nil, err
	}
	warnings, err := extractSkillSource(sourcePath, sourceName, extractDir)
	if err != nil {
		cleanup()
		return "", cleanup, "", warnings, err
	}
	return extractDir, cleanup, sourceName, warnings, nil
}

func downloadSkillImportURL(ctx context.Context, rawURL, workDir string) (string, string, func(), error) {
	sanitized, err := validateSkillImportURL(ctx, rawURL)
	if err != nil {
		return "", "", func() {}, err
	}
	parsed, _ := url.Parse(sanitized)
	sourceName := path.Base(parsed.Path)
	if sourceName == "." || sourceName == "/" || sourceName == "" {
		sourceName = parsed.Hostname() + ".zip"
	}
	client := &http.Client{
		Timeout: 60 * time.Second,
		Transport: &http.Transport{DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
			host, _, err := net.SplitHostPort(address)
			if err != nil {
				return nil, err
			}
			if err := rejectUnsafeSkillImportHost(ctx, host); err != nil {
				return nil, err
			}
			var dialer net.Dialer
			return dialer.DialContext(ctx, network, address)
		}},
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, sanitized, nil)
	if err != nil {
		return "", "", func() {}, codedInvalidArgument(SkillImportErrorInvalidSource, err.Error())
	}
	resp, err := client.Do(req)
	if err != nil {
		return "", "", func() {}, codedInvalidArgument(SkillImportErrorDownloadFailed, err.Error())
	}
	defer resp.Body.Close()
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return "", "", func() {}, codedInvalidArgument(SkillImportErrorDownloadFailed, fmt.Sprintf("download returned HTTP %d", resp.StatusCode))
	}
	if resp.ContentLength > MaxSkillImportBytes {
		return "", "", func() {}, codedInvalidArgument(SkillImportErrorArchiveLimitExceeded, "skill import archive is too large")
	}
	file, err := os.CreateTemp(workDir, "meshx-skill-import-download-*")
	if err != nil {
		return "", "", func() {}, err
	}
	path := file.Name()
	cleanup := func() { _ = os.Remove(path) }
	written, copyErr := io.Copy(file, io.LimitReader(resp.Body, MaxSkillImportBytes+1))
	closeErr := file.Close()
	if copyErr != nil {
		cleanup()
		return "", "", func() {}, codedInvalidArgument(SkillImportErrorDownloadFailed, copyErr.Error())
	}
	if closeErr != nil {
		cleanup()
		return "", "", func() {}, closeErr
	}
	if written > MaxSkillImportBytes {
		cleanup()
		return "", "", func() {}, codedInvalidArgument(SkillImportErrorArchiveLimitExceeded, "skill import archive is too large")
	}
	return path, sourceName, cleanup, nil
}

func validateSkillImportURL(ctx context.Context, raw string) (string, error) {
	resolved, err := resolveSkillImportURL(raw)
	if err != nil {
		return "", err
	}
	parsed, err := url.Parse(resolved)
	if err != nil || parsed.Scheme == "" || parsed.Host == "" {
		return "", codedInvalidArgument(SkillImportErrorInvalidSource, "url must be an absolute HTTP or HTTPS URL")
	}
	if parsed.Scheme != "http" && parsed.Scheme != "https" {
		return "", codedInvalidArgument(SkillImportErrorUnsupportedScheme, "skill imports require HTTP or HTTPS URLs")
	}
	if parsed.User != nil {
		return "", codedInvalidArgument(SkillImportErrorInvalidSource, "url must not include plaintext credentials")
	}
	if !isSupportedSkillImportSourceName(path.Base(parsed.Path)) {
		return "", codedInvalidArgument(SkillImportErrorUnsupportedArchive, "skill import URL must point to a .zip, .tar.gz, .tgz, .tar, .md, or .markdown source")
	}
	if err := rejectUnsafeSkillImportHost(ctx, parsed.Hostname()); err != nil {
		return "", err
	}
	parsed.Fragment = ""
	return parsed.String(), nil
}

func resolveSkillImportURL(raw string) (string, error) {
	raw = strings.TrimSpace(raw)
	if strings.HasPrefix(raw, "git@") {
		parts := strings.SplitN(raw, ":", 2)
		if len(parts) != 2 {
			return "", codedInvalidArgument(SkillImportErrorInvalidSource, fmt.Sprintf("invalid git SSH URL %q", raw))
		}
		host := strings.TrimPrefix(parts[0], "git@")
		repoPath := strings.TrimSuffix(parts[1], ".git")
		switch host {
		case "github.com":
			return fmt.Sprintf("https://github.com/%s/archive/refs/heads/main.zip", repoPath), nil
		case "gitlab.com":
			repo := path.Base(repoPath)
			return fmt.Sprintf("https://gitlab.com/%s/-/archive/main/%s-main.zip", repoPath, repo), nil
		case "gitee.com":
			return fmt.Sprintf("https://gitee.com/%s/repository/archive/main.zip", repoPath), nil
		case "gitcode.com":
			repo := path.Base(repoPath)
			return fmt.Sprintf("https://gitcode.com/%s/-/archive/main/%s-main.zip", repoPath, repo), nil
		default:
			return "", codedInvalidArgument(SkillImportErrorUnsupportedArchive, fmt.Sprintf("unsupported git host %q; only github.com, gitlab.com, gitee.com, and gitcode.com are supported", host))
		}
	}

	parsed, err := url.Parse(raw)
	if err != nil || parsed.Scheme == "" || parsed.Host == "" {
		return "", codedInvalidArgument(SkillImportErrorInvalidSource, "url must be an absolute HTTP or HTTPS URL")
	}
	if parsed.Scheme != "http" && parsed.Scheme != "https" {
		return "", codedInvalidArgument(SkillImportErrorUnsupportedScheme, "skill imports require HTTP or HTTPS URLs")
	}
	pathWithoutGit := strings.TrimSuffix(parsed.Path, ".git")
	switch parsed.Hostname() {
	case "github.com":
		return resolveGitHubImportURL(pathWithoutGit, parsed.RawQuery)
	case "gitlab.com":
		return resolveGitLabImportURL(pathWithoutGit)
	case "gitee.com":
		return resolveGiteeImportURL(pathWithoutGit)
	case "gitcode.com":
		return resolveGitCodeImportURL(pathWithoutGit)
	default:
		if isSupportedSkillImportSourceName(path.Base(pathWithoutGit)) {
			return raw, nil
		}
		return "", codedInvalidArgument(SkillImportErrorUnsupportedArchive, fmt.Sprintf("online import is not supported for %q; use GitHub, GitLab, Gitee, GitCode, or a direct skill archive/markdown URL", parsed.Hostname()))
	}
}

func resolveGitHubImportURL(urlPath, query string) (string, error) {
	parts := splitSkillImportURLPath(urlPath)
	switch {
	case len(parts) == 2:
		return fmt.Sprintf("https://github.com/%s/%s/archive/refs/heads/main.zip", parts[0], parts[1]), nil
	case len(parts) >= 4 && parts[2] == "tree":
		branch := strings.Join(parts[3:], "/")
		return fmt.Sprintf("https://github.com/%s/%s/archive/refs/heads/%s.zip", parts[0], parts[1], branch), nil
	default:
		if query == "" {
			return fmt.Sprintf("https://github.com%s", urlPath), nil
		}
		return fmt.Sprintf("https://github.com%s?%s", urlPath, query), nil
	}
}

func resolveGitLabImportURL(urlPath string) (string, error) {
	parts := splitSkillImportURLPath(urlPath)
	if len(parts) < 2 {
		return "", codedInvalidArgument(SkillImportErrorInvalidSource, fmt.Sprintf("invalid GitLab URL path %q", urlPath))
	}
	for i := 2; i < len(parts)-1; i++ {
		if parts[i] == "-" && parts[i+1] == "tree" {
			repoParts := parts[:i]
			branch := "main"
			if i+2 < len(parts) {
				branch = strings.Join(parts[i+2:], "/")
			}
			repo := parts[i-1]
			return fmt.Sprintf("https://gitlab.com/%s/-/archive/%s/%s-%s.zip", strings.Join(repoParts, "/"), branch, repo, strings.ReplaceAll(branch, "/", "-")), nil
		}
	}
	repo := parts[len(parts)-1]
	return fmt.Sprintf("https://gitlab.com/%s/-/archive/main/%s-main.zip", strings.Join(parts, "/"), repo), nil
}

func resolveGiteeImportURL(urlPath string) (string, error) {
	parts := splitSkillImportURLPath(urlPath)
	if len(parts) < 2 {
		return "", codedInvalidArgument(SkillImportErrorInvalidSource, fmt.Sprintf("invalid Gitee URL path %q", urlPath))
	}
	if len(parts) >= 4 && parts[2] == "tree" {
		return fmt.Sprintf("https://gitee.com/%s/%s/repository/archive/%s.zip", parts[0], parts[1], strings.Join(parts[3:], "/")), nil
	}
	return fmt.Sprintf("https://gitee.com/%s/%s/repository/archive/main.zip", parts[0], parts[1]), nil
}

func resolveGitCodeImportURL(urlPath string) (string, error) {
	parts := splitSkillImportURLPath(urlPath)
	if len(parts) < 2 {
		return "", codedInvalidArgument(SkillImportErrorInvalidSource, fmt.Sprintf("invalid GitCode URL path %q", urlPath))
	}
	for i := 2; i < len(parts)-1; i++ {
		if parts[i] == "-" && parts[i+1] == "tree" {
			repoParts := parts[:i]
			branch := "main"
			if i+2 < len(parts) {
				branch = strings.Join(parts[i+2:], "/")
			}
			repo := parts[i-1]
			return fmt.Sprintf("https://gitcode.com/%s/-/archive/%s/%s-%s.zip", strings.Join(repoParts, "/"), branch, repo, strings.ReplaceAll(branch, "/", "-")), nil
		}
	}
	repo := parts[len(parts)-1]
	return fmt.Sprintf("https://gitcode.com/%s/-/archive/main/%s-main.zip", strings.Join(parts, "/"), repo), nil
}

func splitSkillImportURLPath(urlPath string) []string {
	trimmed := strings.Trim(urlPath, "/")
	if trimmed == "" {
		return nil
	}
	rawParts := strings.Split(trimmed, "/")
	parts := make([]string, 0, len(rawParts))
	for _, part := range rawParts {
		if part != "" {
			parts = append(parts, part)
		}
	}
	return parts
}

func rejectUnsafeSkillImportHost(ctx context.Context, host string) error {
	host = strings.TrimSpace(host)
	if host == "" {
		return codedInvalidArgument(SkillImportErrorInvalidSource, "url host is required")
	}
	if strings.EqualFold(host, "localhost") {
		return codedInvalidArgument(SkillImportErrorUnsafeRemoteHost, "url host must not be localhost")
	}
	if ip := net.ParseIP(host); ip != nil {
		if isUnsafeSkillImportIP(ip) {
			return codedInvalidArgument(SkillImportErrorUnsafeRemoteHost, "url host resolves to a private or local address")
		}
		return nil
	}
	resolver := net.DefaultResolver
	ips, err := resolver.LookupIP(ctx, "ip", host)
	if err != nil || len(ips) == 0 {
		return codedInvalidArgument(SkillImportErrorUnsafeRemoteHost, "url host must resolve before download")
	}
	for _, ip := range ips {
		if isUnsafeSkillImportIP(ip) {
			return codedInvalidArgument(SkillImportErrorUnsafeRemoteHost, "url host resolves to a private or local address")
		}
	}
	return nil
}

func isUnsafeSkillImportIP(ip net.IP) bool {
	return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsUnspecified()
}

func extractSkillSource(filePath, sourceName, targetDir string) ([]SkillImportWarning, error) {
	name := strings.ToLower(strings.TrimSpace(sourceName))
	switch {
	case strings.HasSuffix(name, ".md") || strings.HasSuffix(name, ".markdown"):
		return extractSkillMarkdown(filePath, targetDir)
	case strings.HasSuffix(name, ".zip"):
		return extractSkillZip(filePath, targetDir, defaultSkillArchiveLimits)
	case strings.HasSuffix(name, ".tar.gz") || strings.HasSuffix(name, ".tgz"):
		return extractSkillTarGz(filePath, targetDir, defaultSkillArchiveLimits)
	case strings.HasSuffix(name, ".tar"):
		return extractSkillTar(filePath, targetDir, defaultSkillArchiveLimits)
	default:
		return nil, codedInvalidArgument(SkillImportErrorUnsupportedArchive, "skill import supports .zip, .tar.gz, .tgz, .tar, .md, and .markdown sources")
	}
}

func isSupportedSkillImportArchiveName(name string) bool {
	name = strings.ToLower(strings.TrimSpace(name))
	return strings.HasSuffix(name, ".zip") || strings.HasSuffix(name, ".tar.gz") || strings.HasSuffix(name, ".tgz") || strings.HasSuffix(name, ".tar")
}

func isSupportedSkillImportSourceName(name string) bool {
	name = strings.ToLower(strings.TrimSpace(name))
	return isSupportedSkillImportArchiveName(name) || strings.HasSuffix(name, ".md") || strings.HasSuffix(name, ".markdown")
}

func extractSkillMarkdown(filePath, targetDir string) ([]SkillImportWarning, error) {
	file, err := os.Open(filePath)
	if err != nil {
		return nil, err
	}
	defer file.Close()
	_, err = writeSkillArchiveFile(targetDir, "SKILL.md", file, maxSkillArchiveEntryBytes)
	return nil, err
}

func extractSkillZip(filePath, targetDir string, limits skillArchiveLimits) ([]SkillImportWarning, error) {
	reader, err := zip.OpenReader(filePath)
	if err != nil {
		return nil, codedInvalidArgument(SkillImportErrorUnsupportedArchive, fmt.Sprintf("open ZIP archive: %v", err))
	}
	defer reader.Close()
	if len(reader.File) > limits.maxEntries {
		return nil, codedInvalidArgument(SkillImportErrorArchiveLimitExceeded, fmt.Sprintf("archive contains too many entries: %d exceeds %d", len(reader.File), limits.maxEntries))
	}
	var extracted int64
	warnings := []SkillImportWarning{}
	for _, file := range reader.File {
		clean, err := validateSkillArchivePath(file.Name)
		if err != nil {
			return warnings, err
		}
		if clean == ".git" || strings.HasPrefix(clean, ".git/") || strings.Contains(clean, "/.git/") {
			continue
		}
		mode := file.FileInfo().Mode()
		if mode.IsDir() {
			continue
		}
		if mode&os.ModeType != 0 {
			continue
		}
		if file.UncompressedSize64 > uint64(limits.maxEntryBytes) {
			warnings = append(warnings, skillArchiveEntrySkippedOversizeWarning(file.Name, int64(file.UncompressedSize64), limits.maxEntryBytes))
			continue
		}
		if extracted > limits.maxExtractedBytes-int64(file.UncompressedSize64) {
			warnings = append(warnings, skillArchiveEntrySkippedExtractedLimitWarning(file.Name, int64(file.UncompressedSize64), limits.maxExtractedBytes))
			continue
		}
		rc, err := file.Open()
		if err != nil {
			return warnings, err
		}
		written, err := writeSkillArchiveFile(targetDir, clean, rc, limits.maxEntryBytes)
		rc.Close()
		if err != nil {
			return warnings, err
		}
		if extracted > limits.maxExtractedBytes-written {
			warnings = append(warnings, skillArchiveEntrySkippedExtractedLimitWarning(file.Name, written, limits.maxExtractedBytes))
			continue
		}
		extracted += written
	}
	return warnings, nil
}

func extractSkillTarGz(filePath, targetDir string, limits skillArchiveLimits) ([]SkillImportWarning, error) {
	file, err := os.Open(filePath)
	if err != nil {
		return nil, err
	}
	defer file.Close()
	gz, err := gzip.NewReader(file)
	if err != nil {
		return nil, codedInvalidArgument(SkillImportErrorUnsupportedArchive, fmt.Sprintf("open gzip archive: %v", err))
	}
	defer gz.Close()
	return extractSkillTarReader(tar.NewReader(gz), targetDir, limits)
}

func extractSkillTar(filePath, targetDir string, limits skillArchiveLimits) ([]SkillImportWarning, error) {
	file, err := os.Open(filePath)
	if err != nil {
		return nil, err
	}
	defer file.Close()
	return extractSkillTarReader(tar.NewReader(file), targetDir, limits)
}

func extractSkillTarReader(reader *tar.Reader, targetDir string, limits skillArchiveLimits) ([]SkillImportWarning, error) {
	var entries int
	var extracted int64
	warnings := []SkillImportWarning{}
	for {
		header, err := reader.Next()
		if err == io.EOF {
			return warnings, nil
		}
		if err != nil {
			return warnings, codedInvalidArgument(SkillImportErrorUnsupportedArchive, fmt.Sprintf("read tar archive: %v", err))
		}
		entries++
		if entries > limits.maxEntries {
			return warnings, codedInvalidArgument(SkillImportErrorArchiveLimitExceeded, fmt.Sprintf("archive contains too many entries: %d exceeds %d", entries, limits.maxEntries))
		}
		clean, err := validateSkillArchivePath(header.Name)
		if err != nil {
			return warnings, err
		}
		if clean == ".git" || strings.HasPrefix(clean, ".git/") || strings.Contains(clean, "/.git/") {
			continue
		}
		if header.Typeflag == tar.TypeDir {
			continue
		}
		if header.Typeflag != tar.TypeReg && header.Typeflag != tar.TypeRegA {
			continue
		}
		if header.Size < 0 || header.Size > limits.maxEntryBytes {
			warnings = append(warnings, skillArchiveEntrySkippedOversizeWarning(header.Name, header.Size, limits.maxEntryBytes))
			continue
		}
		if extracted > limits.maxExtractedBytes-header.Size {
			warnings = append(warnings, skillArchiveEntrySkippedExtractedLimitWarning(header.Name, header.Size, limits.maxExtractedBytes))
			continue
		}
		written, err := writeSkillArchiveFile(targetDir, clean, reader, limits.maxEntryBytes)
		if err != nil {
			return warnings, err
		}
		if extracted > limits.maxExtractedBytes-written {
			warnings = append(warnings, skillArchiveEntrySkippedExtractedLimitWarning(header.Name, written, limits.maxExtractedBytes))
			continue
		}
		extracted += written
	}
}

func skillArchiveEntrySkippedOversizeWarning(name string, sizeBytes, limitBytes int64) SkillImportWarning {
	return SkillImportWarning{
		Code:       SkillImportWarningArchiveEntrySkippedOversize,
		Message:    fmt.Sprintf("archive entry %q exceeds %d bytes and was skipped", name, limitBytes),
		Path:       name,
		SizeBytes:  sizeBytes,
		LimitBytes: limitBytes,
	}
}

func skillArchiveEntrySkippedExtractedLimitWarning(name string, sizeBytes, limitBytes int64) SkillImportWarning {
	return SkillImportWarning{
		Code:       SkillImportWarningArchiveEntrySkippedExtractedLimit,
		Message:    fmt.Sprintf("archive entry %q would exceed extracted content limit %d bytes and was skipped", name, limitBytes),
		Path:       name,
		SizeBytes:  sizeBytes,
		LimitBytes: limitBytes,
	}
}

func validateSkillArchivePath(name string) (string, error) {
	if strings.TrimSpace(name) == "" {
		return "", codedInvalidArgument(SkillImportErrorArchiveUnsafe, "empty archive entry path")
	}
	if strings.Contains(name, `\`) || strings.Contains(name, ":") || path.IsAbs(name) {
		return "", codedInvalidArgument(SkillImportErrorArchiveUnsafe, fmt.Sprintf("unsafe archive entry %q", name))
	}
	for _, part := range strings.Split(name, "/") {
		if part == ".." {
			return "", codedInvalidArgument(SkillImportErrorArchiveUnsafe, fmt.Sprintf("unsafe archive entry %q: path traversal is not allowed", name))
		}
	}
	clean := path.Clean(name)
	if clean == "." {
		return "", codedInvalidArgument(SkillImportErrorArchiveUnsafe, fmt.Sprintf("unsafe archive entry %q", name))
	}
	return clean, nil
}

func writeSkillArchiveFile(root, rel string, reader io.Reader, maxBytes int64) (int64, error) {
	rootAbs, err := filepath.Abs(root)
	if err != nil {
		return 0, err
	}
	target := filepath.Join(rootAbs, filepath.FromSlash(rel))
	relToRoot, err := filepath.Rel(rootAbs, target)
	if err != nil {
		return 0, err
	}
	if relToRoot == ".." || strings.HasPrefix(relToRoot, ".."+string(filepath.Separator)) || filepath.IsAbs(relToRoot) {
		return 0, codedInvalidArgument(SkillImportErrorArchiveUnsafe, fmt.Sprintf("unsafe archive entry %q: path escapes import directory", rel))
	}
	if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
		return 0, err
	}
	file, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
	if err != nil {
		return 0, err
	}
	written, copyErr := io.Copy(file, io.LimitReader(reader, maxBytes+1))
	closeErr := file.Close()
	if copyErr != nil {
		return written, copyErr
	}
	if closeErr != nil {
		return written, closeErr
	}
	if written > maxBytes {
		return written, codedInvalidArgument(SkillImportErrorArchiveLimitExceeded, fmt.Sprintf("archive entry %q exceeds %d bytes", rel, maxBytes))
	}
	return written, nil
}
