package archiver

import (
	"archive/tar"
	"compress/gzip"
	"context"
	"crypto/tls"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"

	"gitea.loveuer.com/yizhisec/pkg3/logger"
)

// Options defines options for downloading and extracting archives
type Options struct {
	// InsecureSkipVerify skips TLS certificate verification (equivalent to wget --no-check-certificate)
	InsecureSkipVerify bool
	// HTTPClient allows providing a custom HTTP client
	HTTPClient *http.Client
	// OnProgress is called during extraction with the current file being extracted
	OnProgress func(filename string, index int, total int)
	// IsGzipped explicitly specifies whether the archive is gzip compressed
	// If nil, auto-detect based on file extension (.tar.gz, .tgz)
	IsGzipped *bool
}

// Option is a functional option for configuring the archiver
type Option func(*Options)

// WithInsecureSkipVerify skips TLS certificate verification
func WithInsecureSkipVerify() Option {
	return func(o *Options) {
		o.InsecureSkipVerify = true
	}
}

// WithHTTPClient sets a custom HTTP client
func WithHTTPClient(client *http.Client) Option {
	return func(o *Options) {
		o.HTTPClient = client
	}
}

// WithProgress sets a progress callback
func WithProgress(callback func(filename string, index int, total int)) Option {
	return func(o *Options) {
		o.OnProgress = callback
	}
}

// WithGzipCompression explicitly sets whether the archive is gzip compressed
func WithGzipCompression(isGzipped bool) Option {
	return func(o *Options) {
		o.IsGzipped = &isGzipped
	}
}

// DownloadAndExtract downloads a tar or tar.gz file from URL and extracts it to destDir
// Supports both .tar and .tar.gz formats
func DownloadAndExtract(ctx context.Context, url, destDir string, opts ...Option) error {
	options := &Options{
		InsecureSkipVerify: false,
	}

	for _, opt := range opts {
		opt(options)
	}

	logger.Debug("开始下载和解压: %s -> %s", url, destDir)

	// Create HTTP client
	client := options.HTTPClient
	if client == nil {
		client = &http.Client{}
		if options.InsecureSkipVerify {
			logger.Debug("TLS 证书验证已禁用")
			client.Transport = &http.Transport{
				TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
			}
		}
	}

	// Download the file
	logger.Debug("发起 HTTP 请求: %s", url)
	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
	if err != nil {
		logger.Debug("创建请求失败: %v", err)
		return fmt.Errorf("failed to create request: %w", err)
	}

	resp, err := client.Do(req)
	if err != nil {
		logger.Debug("下载失败: %v", err)
		return fmt.Errorf("failed to download: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		logger.Debug("HTTP 状态码异常: %d", resp.StatusCode)
		return fmt.Errorf("bad status: %s", resp.Status)
	}

	logger.Debug("下载成功，准备解压")

	// Determine if the file is gzipped
	var reader io.Reader = resp.Body
	var isGzipped bool

	if options.IsGzipped != nil {
		// Explicitly specified by option
		isGzipped = *options.IsGzipped
		logger.Debug("压缩格式由选项指定: isGzipped=%v", isGzipped)
	} else {
		// Auto-detect based on file extension
		isGzipped = strings.HasSuffix(url, ".tar.gz") || strings.HasSuffix(url, ".tgz")
		logger.Debug("根据文件扩展名自动检测压缩格式: isGzipped=%v", isGzipped)
	}

	if isGzipped {
		logger.Debug("使用 gzip 解压")
		gzReader, err := gzip.NewReader(resp.Body)
		if err != nil {
			logger.Debug("创建 gzip reader 失败: %v", err)
			return fmt.Errorf("failed to create gzip reader: %w", err)
		}
		defer gzReader.Close()
		reader = gzReader
	} else {
		logger.Debug("tar 格式（未压缩）")
	}

	// Extract tar archive
	if err := extractTar(reader, destDir, options); err != nil {
		return fmt.Errorf("failed to extract tar: %w", err)
	}

	logger.Debug("解压完成")
	return nil
}

// extractTar extracts a tar archive to the destination directory
func extractTar(r io.Reader, destDir string, options *Options) error {
	tarReader := tar.NewReader(r)
	fileCount := 0
	totalFiles := 0

	// First pass: count total files (optional, for progress reporting)
	// For now, we'll just extract directly

	for {
		header, err := tarReader.Next()
		if err == io.EOF {
			break
		}
		if err != nil {
			logger.Debug("读取 tar 条目失败: %v", err)
			return fmt.Errorf("failed to read tar header: %w", err)
		}

		target := filepath.Join(destDir, header.Name)
		logger.Debug("解压文件: %s", header.Name)

		// Call progress callback if provided
		if options.OnProgress != nil {
			options.OnProgress(header.Name, fileCount, totalFiles)
		}

		switch header.Typeflag {
		case tar.TypeDir:
			// Create directory
			if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil {
				logger.Debug("创建目录失败 %s: %v", target, err)
				return fmt.Errorf("failed to create directory %s: %w", target, err)
			}

		case tar.TypeReg:
			// Create regular file
			// Ensure parent directory exists
			if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
				logger.Debug("创建父目录失败 %s: %v", filepath.Dir(target), err)
				return fmt.Errorf("failed to create parent directory: %w", err)
			}

			outFile, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.FileMode(header.Mode))
			if err != nil {
				logger.Debug("创建文件失败 %s: %v", target, err)
				return fmt.Errorf("failed to create file %s: %w", target, err)
			}

			if _, err := io.Copy(outFile, tarReader); err != nil {
				outFile.Close()
				logger.Debug("写入文件失败 %s: %v", target, err)
				return fmt.Errorf("failed to write file %s: %w", target, err)
			}
			outFile.Close()
			fileCount++

		case tar.TypeSymlink:
			// Create symbolic link
			if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
				logger.Debug("创建符号链接父目录失败 %s: %v", filepath.Dir(target), err)
				return fmt.Errorf("failed to create parent directory for symlink: %w", err)
			}

			// Remove existing file/link if exists
			os.Remove(target)

			if err := os.Symlink(header.Linkname, target); err != nil {
				logger.Debug("创建符号链接失败 %s -> %s: %v", target, header.Linkname, err)
				return fmt.Errorf("failed to create symlink %s -> %s: %w", target, header.Linkname, err)
			}
			fileCount++

		default:
			logger.Debug("跳过不支持的文件类型: %s (type: %v)", header.Name, header.Typeflag)
		}
	}

	logger.Debug("解压完成，共 %d 个文件", fileCount)
	return nil
}
