// Copyright (c) 2023 RethinkDNS and its authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package rnet

import (
	"context"
	"errors"
	"fmt"
	"sync"

	x "github.com/celzero/firestack/intra/backend"
	"github.com/celzero/firestack/intra/ipn"
	"github.com/celzero/firestack/intra/log"
	"github.com/celzero/firestack/intra/protect"
)

const (
	// type of services
	SVCSOCKS5 = x.SVCSOCKS5
	SVCHTTP   = x.SVCHTTP
	PXSOCKS5  = x.PXSOCKS5
	PXHTTP    = x.PXHTTP

	// status of proxies
	SUP = x.SUP
	SOK = x.SOK
	SKO = x.SKO
	END = x.SOP
)

var (
	errNoServer    = errors.New("svc: no such server")
	errSvcRunning  = errors.New("svc: service is running")
	errNotUdp      = errors.New("svc: not udp conn")
	errNotTcp      = errors.New("svc: not tcp conn")
	errNoAddr      = errors.New("svc: no address")
	errServerEnd   = errors.New("svc: server stopped")
	errProxyEnd    = errors.New("svc: proxy stopped")
	errProxyPaused = errors.New("svc: proxy paused")
	errNotProxy    = errors.New("svc: not a proxy")
	errBlocked     = errors.New("svc: blocked")

	udptimeoutsec = 5 * 60                    // 5m
	tcptimeoutsec = (2 * 60 * 60) + (40 * 60) // 2h40m
)

// todo: github.com/txthinking/brook/blob/master/pac.go

type Server x.Server

type Services x.Services

type ServerListener x.ServerListener

var _ Services = (*services)(nil)
var _ Server = (*httpx)(nil)
var _ Server = (*socks5)(nil)

type services struct {
	sync.RWMutex
	servers  map[string]Server
	proxies  ipn.Proxies
	listener ServerListener
	ctl      protect.Controller
}

func NewServices(pctx context.Context, proxies ipn.Proxies, ctl protect.Controller, listener ServerListener) *services {
	if listener == nil || ctl == nil {
		return nil
	}
	svc := &services{
		servers:  make(map[string]Server),
		ctl:      ctl,
		proxies:  proxies,
		listener: listener,
	}
	context.AfterFunc(pctx, svc.stopServers)
	return svc
}

func (s *services) AddServer(id, url string) (svc x.Server, err error) {
	s.RemoveServer(id)

	switch id {
	case SVCSOCKS5, PXSOCKS5:
		svc, err = newSocks5Server(id, url, s.ctl, s.listener)
	case SVCHTTP, PXHTTP:
		svc, err = newHttpServer(id, url, s.ctl, s.listener)
	default:
		return nil, errors.ErrUnsupported
	}

	if err != nil {
		return nil, err
	}

	s.Lock()
	s.servers[id] = svc
	s.Unlock()

	// if the server has a namesake proxy, bridge them
	err = s.Bridge(id, id)

	log.I("svc: add: %s > %s; err? %v", id, url, err)

	return svc, err
}

func (s *services) Bridge(serverid, proxyid string) (err error) {
	svc, err := s.GetServer(serverid)

	if err != nil {
		log.W("svc: bridge: no server %s; err? %v", serverid, err)
		return
	}
	// remove existing bridge, if any
	if len(proxyid) <= 0 {
		err = svc.Hop(nil)
		log.I("svc: bridge: remove all hops for %s; err? %v", serverid, err)
		return
	}

	px, err := s.proxies.ProxyFor(proxyid)
	if err != nil {
		log.W("svc: bridge: no proxy %s for %s; err? %v", proxyid, serverid, err)
		return
	}

	svcstr := fmt.Sprintf("%s/%s [%d] at %s", serverid, svc.Type(), svc.Status(), svc.GetAddr())
	pxstr := fmt.Sprintf("%s/%s [%d] at %s", proxyid, px.Type(), px.Status(), px.GetAddr())

	err = svc.Hop(px)

	log.I("svc: bridge: %s with %s; hop err? %v", svcstr, pxstr, err)

	return
}

func (s *services) RemoveServer(id string) bool {
	if svc, err := s.GetServer(id); err == nil {
		_ = svc.Stop()
		delete(s.servers, id)
		return true
	}
	return false
}

func (s *services) GetServer(id string) (x.Server, error) {
	s.RLock()
	defer s.RUnlock()

	if svc, ok := s.servers[id]; ok {
		return svc, nil
	}
	return nil, errNoServer
}

func (s *services) stopServers() {
	s.Lock()
	defer s.Unlock()

	n := len(s.servers)
	for _, svc := range s.servers {
		_ = svc.Stop()
	}
	log.I("svc: stopped servers: %d", n)
}

func (s *services) RefreshServers() string {
	s.Lock()
	defer s.Unlock()

	var csv string
	for _, svc := range s.servers {
		sid := svc.ID()
		if err := svc.Refresh(); err != nil {
			log.W("svc: refresh %s; err: %v", sid, err)
			continue
		}
		if csv == "" {
			csv = sid
		} else {
			csv += "," + sid
		}
	}
	return csv
}

func (s *services) RemoveAll() {
	s.stopServers()

	s.Lock()
	clear(s.servers)
	s.Unlock()
}
