package skills

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"strings"
	"time"

	"github.com/dapicom-ai/omnipus/pkg/security"
	"github.com/dapicom-ai/omnipus/pkg/utils"
)

// GitHubContent represents a file or directory in GitHub API response
type GitHubContent struct {
	Name        string `json:"name"`
	Path        string `json:"path"`
	Type        string `json:"type"` // "file" or "dir"
	DownloadURL string `json:"download_url"`
	URL         string `json:"url"` // API URL for subdirectories
}

// GitHubRef represents a parsed GitHub reference
type GitHubRef struct {
	Owner    string // Repository owner
	RepoName string // Repository name
	Ref      string // Git reference (branch, tag, or commit)
	SubPath  string // Path within the repository
}

type SkillInstaller struct {
	workspace   string
	client      *http.Client
	githubToken string
	proxy       string
}

// NewSkillInstaller creates a new skill installer.
// proxy is an optional HTTP/HTTPS/SOCKS5 proxy URL for downloading skills.
// To enforce SSRF protection (SEC-24), use NewSkillInstallerWithSSRF instead.
func NewSkillInstaller(workspace, githubToken, proxy string) (*SkillInstaller, error) {
	return NewSkillInstallerWithSSRF(workspace, githubToken, proxy, nil)
}

// NewSkillInstallerWithSSRF creates a new skill installer with SSRF protection.
//
// When ssrf is non-nil, all outbound HTTP requests (GitHub API calls, raw
// content downloads) are routed through ssrf.SafeClient(), which blocks
// connections to private/internal IP ranges, cloud metadata endpoints, and
// non-http(s) schemes (SEC-24). Pass nil to use the default proxy-aware client
// without SSRF enforcement.
func NewSkillInstallerWithSSRF(
	workspace, githubToken, proxy string,
	ssrf *security.SSRFChecker,
) (*SkillInstaller, error) {
	var client *http.Client
	if ssrf != nil {
		client = ssrf.SafeClient()
	} else {
		var err error
		client, err = utils.CreateHTTPClient(proxy, 15*time.Second)
		if err != nil {
			return nil, fmt.Errorf("failed to create HTTP client: %w", err)
		}
	}

	return &SkillInstaller{
		workspace:   workspace,
		client:      client,
		githubToken: githubToken,
		proxy:       proxy,
	}, nil
}

// SetHTTPClient replaces the HTTP client used for GitHub downloads.
// Use security.SSRFChecker.SafeClient() to enforce SSRF protection (SEC-24).
func (si *SkillInstaller) SetHTTPClient(client *http.Client) {
	si.client = client
}

// parseGitHubRef parses a GitHub reference.
// Supports: "owner/repo", "owner/repo/path", or full URL like "https://github.com/owner/repo/tree/ref/path"
func parseGitHubRef(repo string) (GitHubRef, error) {
	repo = strings.TrimSpace(repo)

	// Handle full URL
	if strings.HasPrefix(repo, "http://") || strings.HasPrefix(repo, "https://") {
		u, err := url.Parse(repo)
		if err != nil {
			return GitHubRef{}, fmt.Errorf("invalid URL: %w", err)
		}
		parts := strings.Split(strings.Trim(u.Path, "/"), "/")
		if len(parts) < 2 {
			return GitHubRef{}, fmt.Errorf("invalid GitHub URL")
		}
		ref := GitHubRef{
			Owner:    parts[0],
			RepoName: parts[1],
			Ref:      "main",
		}
		// Look for /tree/ or /blob/ in the path
		for i := 2; i < len(parts); i++ {
			if parts[i] == "tree" || parts[i] == "blob" {
				if i+1 < len(parts) {
					ref.Ref = parts[i+1]
					ref.SubPath = strings.Join(parts[i+2:], "/")
				}
				break
			}
		}
		return ref, nil
	}

	// Handle shorthand format
	parts := strings.Split(strings.Trim(repo, "/"), "/")
	if len(parts) < 2 {
		return GitHubRef{}, fmt.Errorf("invalid format %q: expected 'owner/repo'", repo)
	}
	ref := GitHubRef{
		Owner:    parts[0],
		RepoName: parts[1],
		Ref:      "main",
	}
	if len(parts) > 2 {
		ref.SubPath = strings.Join(parts[2:], "/")
	}
	return ref, nil
}

func (si *SkillInstaller) InstallFromGitHub(ctx context.Context, repo string) error {
	ref, err := parseGitHubRef(repo)
	if err != nil {
		return err
	}

	skillName := ref.RepoName
	if ref.SubPath != "" {
		skillName = filepath.Base(ref.SubPath)
	}
	skillDirectory := filepath.Join(si.workspace, "skills", skillName)

	if _, err := os.Stat(skillDirectory); err == nil {
		return fmt.Errorf("skill '%s' already exists", skillName)
	}

	// Build GitHub API URL
	apiPath := path.Join(ref.Owner, ref.RepoName, "contents")
	if ref.SubPath != "" {
		apiPath = path.Join(apiPath, ref.SubPath)
	}
	apiURL := fmt.Sprintf("https://api.github.com/repos/%s?ref=%s", apiPath, ref.Ref)

	if err := si.getGithubDirAllFiles(ctx, apiURL, skillDirectory, true); err != nil {
		// Fallback to raw download
		return si.downloadRaw(ctx, ref.Owner, ref.RepoName, ref.Ref, ref.SubPath, skillDirectory)
	}

	if _, err := os.Stat(filepath.Join(skillDirectory, "SKILL.md")); err != nil {
		return fmt.Errorf("SKILL.md not found in repository")
	}
	return nil
}

// downloadDir recursively downloads a directory from GitHub API
// isRoot: true if this is the skill root directory (only download SKILL.md at root)
func (si *SkillInstaller) getGithubDirAllFiles(ctx context.Context, apiURL, localDir string, isRoot bool) error {
	req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil)
	if err != nil {
		return err
	}
	if si.githubToken != "" {
		req.Header.Set("Authorization", "Bearer "+si.githubToken)
	}

	resp, err := utils.DoRequestWithRetry(si.client, req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		return fmt.Errorf("HTTP %d", resp.StatusCode)
	}

	var items []GitHubContent
	if err := json.NewDecoder(resp.Body).Decode(&items); err != nil {
		return err
	}

	for _, item := range items {
		// Guard against path traversal via malicious filenames from GitHub API.
		if !filepath.IsLocal(item.Name) {
			return fmt.Errorf("unsafe filename in repository: %q", item.Name)
		}
		localPath := filepath.Join(localDir, item.Name)

		switch item.Type {
		case "file":
			if !shouldDownload(item.Name, isRoot) {
				continue
			}
			if err := si.downloadFile(ctx, item.DownloadURL, localPath); err != nil {
				return fmt.Errorf("download %s: %w", item.Name, err)
			}
		case "dir":
			if !isSkillDirectory(item.Name) {
				continue
			}
			if err := si.getGithubDirAllFiles(ctx, item.URL, localPath, false); err != nil {
				return err
			}
		}
	}
	return nil
}

// downloadRaw is a fallback that downloads just SKILL.md from raw.githubusercontent.com
func (si *SkillInstaller) downloadRaw(ctx context.Context, owner, repo, ref, subPath, localDir string) error {
	urlPath := path.Join(owner, repo, ref)
	if subPath != "" {
		urlPath = path.Join(urlPath, subPath)
	}
	url := fmt.Sprintf("https://raw.githubusercontent.com/%s/SKILL.md", urlPath)

	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
	if err != nil {
		return fmt.Errorf("failed to create request: %w", err)
	}

	// Use chunked download to temporary file.
	tmpPath, err := utils.DownloadToFile(ctx, si.client, req, 0)
	if err != nil {
		return fmt.Errorf("failed to fetch skill: %w", err)
	}
	defer os.Remove(tmpPath)

	if err := os.MkdirAll(localDir, 0o755); err != nil {
		return fmt.Errorf("failed to create skill directory: %w", err)
	}

	localPath := filepath.Join(localDir, "SKILL.md")

	// Atomic move from temp to final location.
	if err := os.Rename(tmpPath, localPath); err != nil {
		return fmt.Errorf("failed to write skill file: %w", err)
	}

	return os.Chmod(localPath, 0o600)
}

func (si *SkillInstaller) downloadFile(ctx context.Context, url, localPath string) error {
	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
	if err != nil {
		return err
	}

	// Use chunked download to temporary file, then move atomically to target.
	tmpPath, err := utils.DownloadToFile(ctx, si.client, req, 0)
	if err != nil {
		return err
	}
	defer os.Remove(tmpPath)

	if err := os.MkdirAll(filepath.Dir(localPath), 0o755); err != nil {
		return err
	}

	// Atomic move from temp to final location.
	if err := os.Rename(tmpPath, localPath); err != nil {
		return fmt.Errorf("failed to move downloaded file: %w", err)
	}

	return os.Chmod(localPath, 0o600)
}

// shouldDownload determines if a file should be downloaded
// root: true if we're at the skill root directory
func shouldDownload(name string, root bool) bool {
	if root {
		return name == "SKILL.md"
	}
	return true
}

// isSkillDir checks if a directory is a standard skill resource directory
func isSkillDirectory(name string) bool {
	switch name {
	case "scripts", "references", "assets", "templates", "docs":
		return true
	}
	return false
}

func (si *SkillInstaller) Uninstall(skillName string) error {
	parts := strings.Split(skillName, "/")
	var finalSkillName string
	for i := len(parts) - 1; i >= 0; i-- {
		if parts[i] != "" {
			finalSkillName = parts[i]
			break
		}
	}
	if finalSkillName == "" {
		finalSkillName = skillName
	}

	skillDir := filepath.Join(si.workspace, "skills", finalSkillName)

	if _, err := os.Stat(skillDir); os.IsNotExist(err) {
		return fmt.Errorf("skill '%s' not found (processed as '%s')", skillName, finalSkillName)
	}

	if err := os.RemoveAll(skillDir); err != nil {
		return fmt.Errorf("failed to remove skill '%s': %w", finalSkillName, err)
	}

	return nil
}
