/*
 * SPDX-License-Identifier: AGPL-3.0-only
 * Copyright (c) 2022-2026, daeuniverse Organization <dae@v2raya.org>
 */

package control

import (
	"context"
	stderrors "errors"
	"sync"
	"time"

	"github.com/daeuniverse/outbound/netproxy"
)

const relayHalfCloseTimeout = 10 * time.Second

type relayCore struct {
	left  netproxy.Conn
	right netproxy.Conn

	copyEngine       relayCopyEngine
	halfCloseTimeout time.Duration
	leftRecord       func(int64)
	rightRecord      func(int64)
}

type relayDirection struct {
	name   string
	src    netproxy.Conn
	dst    netproxy.Conn
	record func(int64)
}

type relayResult struct {
	dir string
	err error
}

func newRelayCore(lConn, rConn netproxy.Conn, engine relayCopyEngine, leftRecord func(int64), rightRecord func(int64)) *relayCore {
	return &relayCore{
		left:             lConn,
		right:            rConn,
		copyEngine:       engine,
		halfCloseTimeout: relayHalfCloseTimeout,
		leftRecord:       leftRecord,
		rightRecord:      rightRecord,
	}
}

func (c *relayCore) run(ctx context.Context) error {
	if ctx == nil {
		ctx = context.Background()
	}
	ctx, cancel := context.WithCancel(ctx)
	watchDone := make(chan struct{})
	defer close(watchDone)
	defer cancel()

	results := make(chan relayResult, 2)
	var forceCloseOnce sync.Once

	forceClose := func() {
		forceCloseOnce.Do(func() {
			past := time.Unix(1, 0)
			// Ignore errors: connections may already be closed or deadline operations
			// may fail on certain connection types. The goal is to unblock pending reads.
			_ = c.left.SetReadDeadline(past)
			_ = c.right.SetReadDeadline(past)
			_ = c.left.Close()
			_ = c.right.Close()
		})
	}

	go func() {
		select {
		case <-ctx.Done():
			forceClose()
		case <-watchDone:
		}
	}()

	runDirection := func(dir relayDirection) {
		_, err := c.copyEngine.Copy(ctx, dir.dst, dir.src, dir.record)

		if wc, ok := dir.dst.(WriteCloser); ok {
			_ = wc.CloseWrite()
		}

		if err != nil {
			// Any directional copy error is treated as terminal for this relay:
			// cancel shared context and force-close both sides to promptly
			// unblock pending reads/writes in the peer direction.
			cancel()
			forceClose()
		} else {
			// Graceful half-close: bound the peer's pending read on dir.dst
			// (which is the source of the opposite direction).
			_ = dir.dst.SetReadDeadline(time.Now().Add(c.halfCloseTimeout))
		}

		results <- relayResult{
			dir: dir.name,
			err: err,
		}
	}

	go runDirection(relayDirection{
		name:   "l2r",
		src:    c.left,
		dst:    c.right,
		record: c.rightRecord,
	})
	go runDirection(relayDirection{
		name:   "r2l",
		src:    c.right,
		dst:    c.left,
		record: c.leftRecord,
	})

	first := <-results
	second := <-results
	return mergeRelayErrors(first.err, second.err)
}

// mergeRelayErrors combines errors from both relay directions.
// Uses errors.Join to preserve both errors for inspection with errors.Is/As.
func mergeRelayErrors(err1, err2 error) error {
	return stderrors.Join(err1, err2)
}
