/*
Copyright © 2026 blacktop

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package download

import (
	"fmt"
	"io"
	"os"
	"sort"
	"strconv"
	"strings"

	"charm.land/bubbles/v2/progress"
	"github.com/AlecAivazis/survey/v2"
	"github.com/AlecAivazis/survey/v2/terminal"
	"github.com/MakeNowJust/heredoc/v2"
	"github.com/apex/log"
	pcccmd "github.com/blacktop/ipsw/internal/commands/pcc"
	"github.com/blacktop/ipsw/internal/download"
	"github.com/blacktop/ipsw/internal/utils"
	"github.com/fatih/color"
	"github.com/mattn/go-isatty"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

func init() {
	DownloadCmd.AddCommand(downloadPccCmd)
	// Download behavior flags
	downloadPccCmd.Flags().String("proxy", "", "HTTP/HTTPS proxy")
	downloadPccCmd.Flags().Bool("insecure", false, "do not verify ssl certs")
	downloadPccCmd.Flags().Bool("skip-all", false, "always skip resumable IPSWs")
	downloadPccCmd.Flags().Bool("resume-all", false, "always resume resumable IPSWs")
	downloadPccCmd.Flags().Bool("restart-all", false, "always restart resumable IPSWs")
	// Command-specific flags
	downloadPccCmd.Flags().BoolP("info", "i", false, "Show PCC Release info")
	downloadPccCmd.Flags().String("version", "", "Filter by OS ProductVersion prefix (e.g. 26.3); resolved via partial-zip, cached")
	downloadPccCmd.Flags().String("os-build", "", "Filter by OS ProductBuildVersion prefix (e.g. 23E5207q); resolved via partial-zip, cached")
	downloadPccCmd.Flags().String("build", "", "Filter by cloudOS build version prefix (e.g. 5E, 5E290)")
	downloadPccCmd.Flags().String("train", "", "Filter by cloudOS build train substring (e.g. LuckE)")
	downloadPccCmd.Flags().String("app", "", "Filter by cloudOS application name (e.g. TIE, 'TIE Proxy')")
	downloadPccCmd.Flags().Bool("vphone", false, "Filter to OS IPSWs containing vphone600 firmware; resolved via partial-zip, cached")
	downloadPccCmd.Flags().BoolP("urls", "u", false, "Show compact release summaries and asset URLs without saving assets")
	downloadPccCmd.Flags().Bool("latest", false, "Show only the newest PCC release after filtering")
	// TODO: write to '/var/root/Library/Application Support/com.apple.security-research.pccvre/instances/<NAME>' to create a PCC VM w/o needing to set the csrutil first
	downloadPccCmd.Flags().StringP("output", "o", "", "Output directory to save files to")
	downloadPccCmd.MarkFlagDirname("output")
	// Bind persistent flags
	viper.BindPFlag("download.pcc.proxy", downloadPccCmd.Flags().Lookup("proxy"))
	viper.BindPFlag("download.pcc.insecure", downloadPccCmd.Flags().Lookup("insecure"))
	viper.BindPFlag("download.pcc.skip-all", downloadPccCmd.Flags().Lookup("skip-all"))
	viper.BindPFlag("download.pcc.resume-all", downloadPccCmd.Flags().Lookup("resume-all"))
	viper.BindPFlag("download.pcc.restart-all", downloadPccCmd.Flags().Lookup("restart-all"))
	// Bind command-specific flags
	viper.BindPFlag("download.pcc.info", downloadPccCmd.Flags().Lookup("info"))
	viper.BindPFlag("download.pcc.version", downloadPccCmd.Flags().Lookup("version"))
	viper.BindPFlag("download.pcc.os-build", downloadPccCmd.Flags().Lookup("os-build"))
	viper.BindPFlag("download.pcc.build", downloadPccCmd.Flags().Lookup("build"))
	viper.BindPFlag("download.pcc.train", downloadPccCmd.Flags().Lookup("train"))
	viper.BindPFlag("download.pcc.app", downloadPccCmd.Flags().Lookup("app"))
	viper.BindPFlag("download.pcc.vphone", downloadPccCmd.Flags().Lookup("vphone"))
	viper.BindPFlag("download.pcc.urls", downloadPccCmd.Flags().Lookup("urls"))
	viper.BindPFlag("download.pcc.latest", downloadPccCmd.Flags().Lookup("latest"))
	viper.BindPFlag("download.pcc.output", downloadPccCmd.Flags().Lookup("output"))
}

// downloadPccCmd represents the pcc command
var downloadPccCmd = &cobra.Command{
	Use:     "pcc [INDEX]",
	Aliases: []string{"p", "vre", "pccvre"},
	Short:   "Download PCC VM files",
	Args:    cobra.MaximumNArgs(1),
	Example: heredoc.Doc(`
		# Show available PCC releases info
		❯ ipsw download pcc --info

		# Show info for specific PCC release by index
		❯ ipsw download pcc 42 --info

		# Download specific PCC release by index
		❯ ipsw download pcc 42

		# Filter by cloudOS build prefix and pick interactively
		❯ ipsw download pcc --build 5E --app TIE

		# Filter by OS BuildManifest ProductBuildVersion prefix
		❯ ipsw download pcc --os-build 23E5207q --info

		# Show only releases whose OS IPSW contains vphone600 firmware
		❯ ipsw download pcc --vphone --info

		# Show compact release summaries and labeled asset URLs
		❯ ipsw download pcc --info --urls

		# Show only the newest vphone release as a compact summary
		❯ ipsw download pcc --vphone --latest

		# Download PCC VM files interactively
		❯ ipsw download pcc

		# Download to specific directory
		❯ ipsw download pcc --output ./pcc-vms
	`),
	SilenceErrors: true,
	RunE: func(cmd *cobra.Command, args []string) error {

		// settings
		proxy := viper.GetString("download.pcc.proxy")
		insecure := viper.GetBool("download.pcc.insecure")
		// skipAll := viper.GetBool("download.pcc.skip-all")
		// resumeAll := viper.GetBool("download.pcc.resume-all")
		// restartAll := viper.GetBool("download.pcc.restart-all")
		versionFilter := viper.GetString("download.pcc.version")
		osBuildFilter := viper.GetString("download.pcc.os-build")
		buildFilter := viper.GetString("download.pcc.build")
		trainFilter := viper.GetString("download.pcc.train")
		appFilter := viper.GetString("download.pcc.app")
		showInfo := viper.GetBool("download.pcc.info")
		vphoneOnly := viper.GetBool("download.pcc.vphone")
		urlsOnly := viper.GetBool("download.pcc.urls")
		latestOnly := viper.GetBool("download.pcc.latest")

		var bar func(done, total uint64)
		var clearBar func()
		if isatty.IsTerminal(os.Stderr.Fd()) {
			p := progress.New(progress.WithDefaultBlend(), progress.WithWidth(40), progress.WithoutPercentage())
			clearBar = func() { fmt.Fprint(os.Stderr, "\r\033[K") }
			bar = func(done, total uint64) {
				fmt.Fprintf(os.Stderr, "\r   • Fetching PCC log %s %d/%d", p.ViewAs(float64(done)/float64(total)), done, total)
				if done >= total {
					clearBar()
				}
			}
		}
		releases, err := download.GetPCCReleases(proxy, insecure, bar)
		if err != nil {
			if clearBar != nil {
				clearBar()
			}
			return err
		}

		if len(args) > 0 {
			index, err := strconv.Atoi(args[0])
			if err != nil {
				return fmt.Errorf("invalid index: %s", args[0])
			}

			// Find release with matching index
			foundIndex := -1
			for i := range releases {
				if releases[i].Index == uint64(index) {
					foundIndex = i
					break
				}
			}

			if foundIndex == -1 {
				return fmt.Errorf("no PCC release found with index %d", index)
			}

			// Replace releases list with filtered single release
			releases = releases[foundIndex : foundIndex+1]
		} else {
			releases = download.UniquePCCReleases(releases)
		}

		if buildFilter != "" || trainFilter != "" || appFilter != "" {
			filtered := releases[:0]
			for _, r := range releases {
				build, train, app := r.CloudOSInfo()
				if buildFilter != "" && !strings.HasPrefix(build, buildFilter) {
					continue
				}
				if trainFilter != "" && !strings.Contains(train, trainFilter) {
					continue
				}
				if appFilter != "" && app != appFilter {
					continue
				}
				filtered = append(filtered, r)
			}
			releases = filtered
		}

		// OS version/build filters require network on first use; run after
		// the cheap metadata filters so we resolve as few URLs as possible.
		// Resolution attaches PCCVersion to each release in-place and
		// persists to the unified pcc_log.json cache.
		if versionFilter != "" || osBuildFilter != "" {
			download.ResolvePCCVersions(releases, pccVersionFetcher(proxy, insecure))
			filtered := releases[:0]
			for _, r := range releases {
				if r.Version == nil {
					continue
				}
				if versionFilter != "" && !strings.HasPrefix(r.Version.Version, versionFilter) {
					continue
				}
				if osBuildFilter != "" && !strings.HasPrefix(r.Version.Build, osBuildFilter) {
					continue
				}
				filtered = append(filtered, r)
			}
			releases = filtered
		}

		if vphoneOnly {
			// VRE metadata only says that the release is research-bootable.
			// Inspect the OS IPSW itself so --vphone returns only releases
			// with confirmed vphone600 firmware, excluding unresolved assets.
			download.ResolveVPhoneFirmware(releases, pccVPhoneFetcher(proxy, insecure))
			releases = filterVPhoneReleases(releases)

			// The ordinary picker avoids paying to resolve every OS version.
			// A --vphone query is already doing remote partial-zip work, so
			// resolve the smaller result set and show useful version labels.
			download.ResolvePCCVersions(releases, pccVersionFetcher(proxy, insecure))
		}

		releases = sortAndLimitPCCReleases(releases, latestOnly)

		if len(releases) == 0 {
			return fmt.Errorf("no PCC Releases found")
		}

		compactOutput := urlsOnly || (latestOnly && !showInfo)
		if showInfo || compactOutput {
			// Warm BuildManifest ProductVersion/ProductBuildVersion so the
			// cloudOS agent build line can include the OS build next to it.
			download.ResolvePCCVersions(releases, pccVersionFetcher(proxy, insecure))
			if compactOutput {
				return writePCCURLSummaries(cmd.OutOrStdout(), releases)
			}

			log.Infof("Found %d PCC Releases", len(releases))
			// Resolve any releases whose VPhone hasn't been populated yet from
			// the cache; new entries since the last run pay the partial-zip
			// cost once and persist the result.
			download.ResolveVPhoneFirmware(releases, pccVPhoneFetcher(proxy, insecure))
			for i := range releases {
				release := releases[i]
				fmt.Println(" ╭╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴")
				fmt.Println(release)
				if release.Version != nil {
					utils.Indent(log.WithFields(pccVersionFields(*release.Version)).Info, 1)("OS IPSW")
				} else if len(releases) == 1 {
					if v, err := pccVersionFetcher(proxy, insecure)(release.OSAssetURL()); err == nil {
						utils.Indent(log.WithFields(pccVersionFields(v)).Info, 1)("OS IPSW")
					}
				}
				printVPhoneBanner(release.VRE(), release.VPhone)
				fmt.Println(" ╰╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴╴")
			}
		} else {
			// Picker reads what's already in the cache via release.VPhone —
			// no network calls. Uncached releases show a blank tag; run
			// --info once to warm the cache for next time.
			var choices []string
			for i := range releases {
				r := releases[i]
				build, train, app := r.CloudOSInfo()
				label := build
				if r.Version != nil {
					label = r.Version.Version + " " + build
				}
				if train != "" {
					label += " (" + train + ")"
				}
				if app != "" {
					label += "  " + app
				}
				if label == "" {
					label = r.ReleaseID()
				}
				choices = append(choices, fmt.Sprintf("%s %05d: %-52s [created: %s]",
					vphoneTag(r.VRE(), r.VPhone),
					r.Index,
					label,
					r.ReleaseCreationTime().Format("2006-01-02 15:04:05"),
				))
			}

			choice := 0
			prompt := &survey.Select{
				Message:  "PCC Release to download:",
				Options:  choices,
				PageSize: 15,
			}
			if err := survey.AskOne(prompt, &choice); err == terminal.InterruptErr {
				log.Warn("Exiting...")
				return nil
			}
			r := releases[choice]
			out := viper.GetString("download.pcc.output")
			if out == "" {
				build, _, _ := r.CloudOSInfo()
				out = fmt.Sprintf("PCC_%d_%s", r.Index, build)
			}
			log.Infof("Downloading PCC Release %d to %s", r.Index, out)
			return r.Download(out, proxy, insecure)
		}

		return nil
	},
}

func sortAndLimitPCCReleases(releases []*download.PCCRelease, latestOnly bool) []*download.PCCRelease {
	// Log index does not track chronology — releases can be appended out
	// of build order. Sort by the metadata timestamp so newest is first.
	sort.Slice(releases, func(i, j int) bool {
		return releases[i].ReleaseCreationTime().After(releases[j].ReleaseCreationTime())
	})
	if latestOnly && len(releases) > 0 {
		return releases[:1]
	}
	return releases
}

func writePCCURLSummaries(w io.Writer, releases []*download.PCCRelease) error {
	for i, release := range releases {
		if i > 0 {
			if _, err := fmt.Fprintln(w); err != nil {
				return fmt.Errorf("failed to write PCC URL summary: %w", err)
			}
		}
		if _, err := fmt.Fprintln(w, pcccmd.ReleaseURLSummary(release)); err != nil {
			return fmt.Errorf("failed to write PCC URL summary: %w", err)
		}
	}
	return nil
}

func filterVPhoneReleases(releases []*download.PCCRelease) []*download.PCCRelease {
	filtered := releases[:0]
	for _, release := range releases {
		if release.VPhone != nil && release.VPhone.Present {
			filtered = append(filtered, release)
		}
	}
	return filtered
}

// pccVPhoneFetcher partial-zips an OS asset's central directory and reports
// whether the IPSW carries vphone600 research-device firmware. Apple shipped
// vphone600 through cloudOS train 5E (iOS 26.3); train 5F (iOS 26.4) dropped
// it, breaking iPhone-shaped virtualization (vphone-cli). Only the zip
// listing is fetched — no file body reads — so this is cheap.
func pccVPhoneFetcher(proxy string, insecure bool) func(url string) (download.VPhoneFirmware, error) {
	return pcccmd.VPhoneFetcher(&download.RemoteConfig{Proxy: proxy, Insecure: insecure})
}

// vphoneState classifies a release into one of four display buckets shared
// by the --info banner and the interactive-picker tag.
type vphoneState int

const (
	vphoneUnknown vphoneState = iota // not in cache, no metadata signal
	vphoneVRE                        // metadata says VRE but firmware not checked
	vphonePresent                    // firmware present in IPSW
	vphoneMissing                    // firmware absent — the alert signal
)

// vphoneDisplay holds the strings + color + log level for each state, so the
// picker tag and the --info banner can't drift on the next edit.
//
// tag is 7 cells wide on wide-emoji terminals; warn drives apex/log color.
var vphoneDisplay = map[vphoneState]struct {
	tag, banner, detail string
	color               func(...any) string
	warn                bool
}{
	vphonePresent: {"📱VPHN ", "📱 VPHONE", "vphone600 firmware present", color.New(color.Bold, color.FgHiGreen).SprintFunc(), false},
	vphoneMissing: {"🚫NONE ", "🚫 NO VPHONE", "vphone600 firmware REMOVED — vresearch-only", color.New(color.Bold, color.FgHiRed).SprintFunc(), true},
	vphoneVRE:     {"⬢VRE   ", "⬢ VRE", "metadata-only marker — IPSW firmware not checked", color.New(color.Bold, color.FgHiYellow).SprintFunc(), true},
	vphoneUnknown: {"       ", "", "", nil, false},
}

func vphoneClassify(vre download.VRESignals, v *download.VPhoneFirmware) vphoneState {
	// Only VRE releases get the missing-firmware alert. A non-VRE release
	// without vphone600 was never expected to have it — silently classify
	// as unknown so the picker tag and --info banner stay clean.
	switch {
	case v != nil && v.Present:
		return vphonePresent
	case v != nil && vre.IsVRE():
		return vphoneMissing
	case vre.IsVRE():
		return vphoneVRE
	default:
		return vphoneUnknown
	}
}

func vphoneTag(vre download.VRESignals, v *download.VPhoneFirmware) string {
	return vphoneDisplay[vphoneClassify(vre, v)].tag
}

// printVPhoneBanner emits a one-line, indented status line at the bottom of
// each --info release block. The missing branch is the alert signal.
func printVPhoneBanner(vre download.VRESignals, v *download.VPhoneFirmware) {
	d := vphoneDisplay[vphoneClassify(vre, v)]
	if d.banner == "" {
		return
	}
	fields := log.Fields{"device": vre.Device}
	if v != nil && v.Count > 0 {
		fields["count"] = v.Count
	}
	msg := d.color(d.banner) + "  " + d.detail
	if d.warn {
		utils.Indent(log.WithFields(fields).Warn, 2)(msg)
	} else {
		utils.Indent(log.WithFields(fields).Info, 2)(msg)
	}
}

func pccVersionFields(v download.PCCVersion) log.Fields {
	fields := log.Fields{"version": v.Version, "build": v.Build}
	if channel := v.Channel(); channel != "" {
		fields["channel"] = channel
	}
	return fields
}

// pccVersionFetcher returns a closure that partial-zips an OS asset to read
// only BuildManifest.plist (info.ParseZipFiles would also pull every other
// plist plus parse all DeviceTree im4p files). ~85% of releases serve Range
// requests; the newest few sometimes 403 before CDN sync.
func pccVersionFetcher(proxy string, insecure bool) func(url string) (download.PCCVersion, error) {
	return pcccmd.VersionFetcher(&download.RemoteConfig{Proxy: proxy, Insecure: insecure})
}
