package main

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
)

const BROKEN_SYMLINK_TAG = "broken-symlink"

type symlinkErr struct {
	pkg    string
	source string
	target string
	err    error
}

func (s symlinkErr) Error() string {
	if s.err != nil {
		return fmt.Sprintf("%s: broken-symlink %s (%v)", s.pkg, s.source, s.err)
	}
	return fmt.Sprintf("%s: %s %s -> %s", s.pkg, BROKEN_SYMLINK_TAG, s.source, s.target)
}

type brokenSymlinkChecker struct {
	emit bool
}

func newBrokenSymlinkChecker(tags tagFilter) brokenSymlinkChecker {
	return brokenSymlinkChecker{
		emit: tags.shouldEmit(BROKEN_SYMLINK_TAG),
	}
}

func (bs brokenSymlinkChecker) check(pkg2files map[string][]string) []error {
	if !bs.emit {
		return nil
	}

	var tags []error
	for pkg, files := range pkg2files {
		for _, f := range files {
			// Verify that file can be stat'ed and is a symlink.
			fi, err := os.Lstat(f)
			if err != nil || !isSymlink(fi) {
				continue
			}

			// Readlink returns the full path `f` directly links to
			// (relative or absolute) -- unlike os.Lstat() which
			// only returns the final component of the target. This
			// is what we want to report regardless of whether there
			// is a chain of symlinks.
			firstTarget, err := os.Readlink(f)
			if os.IsPermission(err) {
				continue
			}
			if err != nil {
				if errors.Is(err, os.ErrNotExist) {
					err = nil
				}
				tags = append(tags, symlinkErr{
					pkg:    pkg,
					source: f,
					target: firstTarget,
					err:    err,
				})
				continue
			}

			// EvalSymlinks() keeps on evaluating until it resolves
			// to a non-symlink.
			finalTarget, err := filepath.EvalSymlinks(f)
			if err != nil {
				if os.IsPermission(err) {
					continue
				}
				if errors.Is(err, os.ErrNotExist) {
					err = nil
				}
				tags = append(tags, symlinkErr{
					pkg:    pkg,
					source: f,
					target: firstTarget,
					err:    err,
				})
				continue
			}
			if pathExists(finalTarget) {
				continue
			}
			tags = append(tags, symlinkErr{
				pkg:    pkg,
				source: f,
				target: firstTarget,
			})
		}
	}

	return tags
}
