// meta4ra -- Metalink 4.0 utilities
// Copyright (C) 2021-2026 Sergey Matveev <stargrave@stargrave.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3 of the License.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package main

import (
	"bufio"
	"encoding/xml"
	"flag"
	"fmt"
	"io"
	"log"
	"os"
	"path"
	"strings"
	"time"

	meta4ra "go.stargrave.org/meta4ra/v2/internal"
)

func runCheck() {
	plain := flag.Bool("plain", false,
		".meta4 is only a plain hashes list. That implies -pipe")
	pipe := flag.Bool("pipe", false, "Verify file data from stdin, copy to stdout")
	progress := flag.Bool("progress", false, "Show progress of piping/downloading")
	allHashes := flag.Bool("all-hashes", false, "Check all hashes, not the first common one")
	hashers := flag.String("hashers", meta4ra.HashersDefault,
		"hash-name:commandline[,...]")
	flag.Usage = func() {
		fmt.Fprintf(flag.CommandLine.Output(),
			"Usage: %s [options] FILE.meta4 [FILE ...]\n", os.Args[0])
		flag.PrintDefaults()
		fmt.Fprint(flag.CommandLine.Output(), `
If no FILEs are specified, then all <file>s from metalink are searched and
verified if they exist. Otherwise only specified FILEs are checked. If you
want to skip any <file> verification (for example only to validate the
format, then you can just specify an empty ("") FILE.
`)
	}
	flag.Parse()

	if *showVersion {
		fmt.Println(meta4ra.Version())
		return
	}
	if *showWarranty {
		fmt.Println(meta4ra.Warranty)
		return
	}

	if flag.NArg() < 1 {
		flag.Usage()
		os.Exit(1)
	}
	var meta meta4ra.Metalink
	{
		data, err := os.ReadFile(flag.Arg(0))
		if err != nil {
			log.Fatal(err)
		}
		if *plain {
			*pipe = true
			meta.Files = []meta4ra.File{{Name: "-"}}
			for line := range strings.SplitSeq(string(data), "\n") {
				cols := strings.Split(line, " ")
				if len(cols) != 2 {
					continue
				}
				meta.Files[0].Hashes = append(meta.Files[0].Hashes,
					meta4ra.Hash{Type: cols[0], Hash: cols[1]})
			}
		} else {
			err = xml.Unmarshal(data, &meta)
			if err != nil {
				log.Fatal(err)
			}
		}
	}

	toCheck := make(map[string]string)
	if *plain {
		toCheck["-"] = "-"
	} else {
		for _, fn := range flag.Args()[1:] {
			toCheck[path.Base(fn)] = fn
		}
		if *pipe && len(toCheck) != 1 {
			log.Fatal("exactly single FILE must be specified when using -pipe")
		}
	}

	bad := false
	for _, f := range meta.Files {
		fullPath := toCheck[f.Name]
		delete(toCheck, f.Name)
		if !(flag.NArg() == 1 || fullPath != "") {
			continue
		}
		if fullPath == "" {
			fullPath = f.Name
		}
		if !*pipe {
			s, err := os.Stat(fullPath)
			if err != nil {
				log.Println(err)
				bad = true
				continue
			}
			if uint64(s.Size()) != f.Size {
				log.Println("size mismatch",
					f.Name, "our:", s.Size(), "their:", f.Size)
				bad = true
				continue
			}
		}

		hasher, err := meta4ra.NewHasher(*hashers)
		if err != nil {
			log.Println(f.Name, err)
			bad = true
			continue
		}
		var hashTheir string
		var hashName string
		if *allHashes {
			goto HashFound
		}
		for i, name := range hasher.Names {
			for _, h := range f.Hashes {
				if h.Type == name {
					hasher.Names = []string{name}
					hasher.Cmds = append(hasher.Cmds[:0], hasher.Cmds[i])
					hasher.Ins = append(hasher.Ins[:0], hasher.Ins[i])
					hasher.Outs = append(hasher.Outs[:0], hasher.Outs[i])
					hashName = name
					hashTheir = h.Hash
					goto HashFound
				}
			}
		}
		log.Println("no common hashes found for:", f.Name)
		bad = true
		continue
	HashFound:

		var src io.ReadCloser
		src = os.Stdin
		if !*pipe {
			src, err = os.Open(fullPath)
			if err != nil {
				log.Println("error:", f.Name, err)
				bad = true
				continue
			}
		}
		err = hasher.Start()
		if err != nil {
			if !*pipe {
				src.Close()
			}
			hasher.Stop()
			log.Println("error:", f.Name, err)
			bad = true
			continue
		}
		var w io.Writer
		if *pipe {
			w = io.MultiWriter(os.Stdout, hasher)
		} else {
			w = hasher
		}
		if *progress {
			bar := Progress{w: w, now: time.Now(), total: f.Size}
			bar.next = bar.now.Add(ProgressPeriod)
			w = &bar
		}
		_, err = io.Copy(w, bufio.NewReaderSize(src, meta4ra.BufLen))
		if !*pipe {
			src.Close()
		}
		if err != nil {
			hasher.Stop()
			log.Println("error:", f.Name, err)
			bad = true
			continue
		}
		sums, err := hasher.Sums()
		if err != nil {
			hasher.Stop()
			log.Println("error:", f.Name, err)
			bad = true
			continue
		}
		if *allHashes {
			hashesOur := make(map[string]string, len(sums))
			for _, h := range sums {
				hashesOur[h.Type] = h.Hash
			}
			for _, h := range f.Hashes {
				hashOur := hashesOur[h.Type]
				if h.Hash == hashOur {
					if !*pipe {
						fmt.Fprintln(os.Stderr, f.Name, h.Type, "good")
					}
				} else {
					log.Println(
						"hash mismatch:", f.Name, h.Type,
						"our:", hashOur,
						"their:", h.Hash,
					)
					bad = true
				}
			}
		} else {
			hashOur := sums[0].Hash
			if hashOur == hashTheir {
				if !*pipe {
					fmt.Fprintln(os.Stderr, f.Name, hashName, "good")
				}
			} else {
				log.Println(
					"hash mismatch:", f.Name, hashName,
					"our:", hashOur,
					"their:", hashTheir,
				)
				bad = true
				continue
			}
		}
	}
	if len(toCheck) != 0 {
		if _, ok := toCheck["."]; !(len(toCheck) == 1 && ok) {
			log.Println("not all FILEs met")
			bad = true
		}
	}
	if bad {
		os.Exit(1)
	}
}
