package service

import (
	"context"
	"crypto/rand"
	"crypto/rsa"
	"encoding/hex"
	"fmt"
	"io"
	"net"
	"regexp"
	"strings"
	"sync"
	"time"

	"golang.org/x/crypto/ssh"
)

// consoleSessionTTL 是会话从创建起的最长存活时间，超时未在用即回收（含云端连接）。
// OCI 侧另有 24 小时强制断开兜底。
const consoleSessionTTL = 15 * time.Minute

// 网页控制台会话类型：serial 走 PTY 交互式会话，vnc 走 5900 端口转发。
const (
	ConsoleTypeSerial = "serial"
	ConsoleTypeVnc    = "vnc"
)

// ConsoleSession 是一次网页控制台会话（串行 / VNC）：私钥只存内存，服务重启即失效。
type ConsoleSession struct {
	ID           string
	Type         string
	cfgID        uint
	instanceID   string
	region       string
	connectionID string // console connection OCID，同时是第一跳 SSH 的用户名
	consoleHost  string // instance-console.<region>.oci.oraclecloud.com
	signer       ssh.Signer
	createdAt    time.Time

	mu    sync.Mutex
	inUse bool
}

// ConsoleService 管理网页控制台会话：创建 OCI 控制台连接、建立两跳 SSH 隧道。
type ConsoleService struct {
	configs      *OciConfigService
	pollInterval time.Duration // 清理残留连接的轮询间隔，测试注入缩短

	mu       sync.Mutex
	sessions map[string]*ConsoleSession
}

func NewConsoleService(configs *OciConfigService) *ConsoleService {
	return &ConsoleService{
		configs:      configs,
		pollInterval: 2 * time.Second,
		sessions:     map[string]*ConsoleSession{},
	}
}

// CreateSession 生成一次性 RSA 密钥对并创建实例控制台连接（等待 ACTIVE）。
// OCI 限制每实例一个控制台连接，创建前先清理实例上的残留连接。
func (s *ConsoleService) CreateSession(ctx context.Context, cfgID uint, instanceID, region, typ string) (*ConsoleSession, error) {
	if typ != ConsoleTypeSerial && typ != ConsoleTypeVnc {
		return nil, fmt.Errorf("unsupported console session type %q (expect serial or vnc)", typ)
	}
	key, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		return nil, fmt.Errorf("generate rsa key: %w", err)
	}
	signer, err := ssh.NewSignerFromKey(key)
	if err != nil {
		return nil, fmt.Errorf("new ssh signer: %w", err)
	}
	if err := s.cleanupStaleConnections(ctx, cfgID, region, instanceID); err != nil {
		return nil, err
	}
	pubKey := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(signer.PublicKey())))
	conn, err := s.configs.CreateConsoleConnection(ctx, cfgID, region, instanceID, pubKey)
	if err != nil {
		return nil, err
	}
	if conn.LifecycleState != "ACTIVE" {
		return nil, fmt.Errorf("console connection not active yet (state %s), retry later", conn.LifecycleState)
	}
	host, err := parseConsoleHost(conn.VncConnectionString)
	if err != nil {
		return nil, err
	}
	return s.storeSession(cfgID, instanceID, region, typ, conn.ID, host, signer), nil
}

// cleanupStaleConnections 删除实例上已有的控制台连接（每实例限一个），
// 并等待其真正消失；DELETED 是终态记录，仅需跳过。
func (s *ConsoleService) cleanupStaleConnections(ctx context.Context, cfgID uint, region, instanceID string) error {
	conns, err := s.configs.ConsoleConnections(ctx, cfgID, region, instanceID)
	if err != nil {
		return fmt.Errorf("list existing console connections: %w", err)
	}
	pending := false
	for _, c := range conns {
		switch c.LifecycleState {
		case "DELETED":
			continue
		case "DELETING":
			pending = true
		default:
			if err := s.configs.DeleteConsoleConnection(ctx, cfgID, region, c.ID); err != nil {
				return fmt.Errorf("delete stale console connection: %w", err)
			}
			pending = true
		}
	}
	if !pending {
		return nil
	}
	return s.waitConnectionsGone(ctx, cfgID, region, instanceID)
}

// waitConnectionsGone 轮询等待实例上的控制台连接全部进入 DELETED，最长约 60 秒。
func (s *ConsoleService) waitConnectionsGone(ctx context.Context, cfgID uint, region, instanceID string) error {
	for i := 0; i < 30; i++ {
		select {
		case <-ctx.Done():
			return fmt.Errorf("wait console connection cleanup: %w", ctx.Err())
		case <-time.After(s.pollInterval):
		}
		conns, err := s.configs.ConsoleConnections(ctx, cfgID, region, instanceID)
		if err != nil {
			return fmt.Errorf("poll console connection cleanup: %w", err)
		}
		gone := true
		for _, c := range conns {
			if c.LifecycleState != "DELETED" {
				gone = false
				break
			}
		}
		if gone {
			return nil
		}
	}
	return fmt.Errorf("cleanup previous console connection timed out, retry later")
}

func (s *ConsoleService) storeSession(cfgID uint, instanceID, region, typ, connID, host string, signer ssh.Signer) *ConsoleSession {
	buf := make([]byte, 16)
	_, _ = rand.Read(buf)
	sess := &ConsoleSession{
		ID:           hex.EncodeToString(buf),
		Type:         typ,
		cfgID:        cfgID,
		instanceID:   instanceID,
		region:       region,
		connectionID: connID,
		consoleHost:  host,
		signer:       signer,
		createdAt:    time.Now(),
	}
	s.mu.Lock()
	s.sessions[sess.ID] = sess
	s.mu.Unlock()
	time.AfterFunc(consoleSessionTTL, func() { s.expire(sess.ID) })
	return sess
}

// expire TTL 到期回收：正在使用的会话跳过（连接断开后自然停止，无续期）。
func (s *ConsoleService) expire(id string) {
	s.mu.Lock()
	sess, ok := s.sessions[id]
	if ok {
		sess.mu.Lock()
		if sess.inUse {
			ok = false
		} else {
			delete(s.sessions, id)
		}
		sess.mu.Unlock()
	}
	s.mu.Unlock()
	if ok {
		s.deleteCloudConnection(sess)
	}
}

// Get 返回会话；不存在或已回收返回 nil。
func (s *ConsoleService) Get(id string) *ConsoleSession {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.sessions[id]
}

// CloseSession 主动结束会话并删除云端控制台连接。
func (s *ConsoleService) CloseSession(id string) {
	s.mu.Lock()
	sess, ok := s.sessions[id]
	delete(s.sessions, id)
	s.mu.Unlock()
	if ok {
		s.deleteCloudConnection(sess)
	}
}

func (s *ConsoleService) deleteCloudConnection(sess *ConsoleSession) {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()
	_ = s.configs.DeleteConsoleConnection(ctx, sess.cfgID, sess.region, sess.connectionID)
}

// MarkUse 标记会话进入 / 退出使用（使用中的会话不被 TTL 回收）。
func (sess *ConsoleSession) MarkUse(v bool) {
	sess.mu.Lock()
	sess.inUse = v
	sess.mu.Unlock()
}

// consoleHostRe 从 OCI 下发的 VNC 连接串里取代理主机：
// ssh -o ProxyCommand='ssh -W %h:%p -p 443 <conn_ocid>@<host>' ...
var consoleHostRe = regexp.MustCompile(`-p\s+443\s+\S+@([A-Za-z0-9.-]+)`)

func parseConsoleHost(vncConnectionString string) (string, error) {
	m := consoleHostRe.FindStringSubmatch(vncConnectionString)
	if m == nil {
		return "", fmt.Errorf("parse console host from vnc connection string failed")
	}
	return m[1], nil
}

// sshConfig 构造两跳共用的客户端配置；代理主机名来自 OCI API 响应，无公开指纹可校验。
func (sess *ConsoleSession) sshConfig(user string) *ssh.ClientConfig {
	return &ssh.ClientConfig{
		User:            user,
		Auth:            []ssh.AuthMethod{ssh.PublicKeys(sess.signer)},
		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
		Timeout:         20 * time.Second,
	}
}

// dialHops 建立两跳 SSH：第一跳到区域控制台代理（443，用户名为连接 OCID），
// 在其上开通道到实例 22 端口完成第二跳握手，返回第二跳客户端。
func (sess *ConsoleSession) dialHops() (hop1, hop2 *ssh.Client, err error) {
	hop1, err = ssh.Dial("tcp", net.JoinHostPort(sess.consoleHost, "443"), sess.sshConfig(sess.connectionID))
	if err != nil {
		return nil, nil, fmt.Errorf("dial console proxy: %w", err)
	}
	pipe, err := hop1.Dial("tcp", net.JoinHostPort(sess.instanceID, "22"))
	if err != nil {
		hop1.Close()
		return nil, nil, fmt.Errorf("open tunnel to instance console endpoint: %w", err)
	}
	conn, chans, reqs, err := ssh.NewClientConn(pipe, sess.instanceID+":22", sess.sshConfig(sess.instanceID))
	if err != nil {
		pipe.Close()
		hop1.Close()
		return nil, nil, fmt.Errorf("second hop ssh handshake: %w", err)
	}
	return hop1, ssh.NewClient(conn, chans, reqs), nil
}

// DialVNC 经第二跳转发到实例 5900 的 VNC 流。
func (sess *ConsoleSession) DialVNC(ctx context.Context) (net.Conn, error) {
	hop1, hop2, err := sess.dialHops()
	if err != nil {
		return nil, err
	}
	vnc, err := hop2.Dial("tcp", net.JoinHostPort(sess.instanceID, "5900"))
	if err != nil {
		hop2.Close()
		hop1.Close()
		return nil, fmt.Errorf("open vnc channel: %w", err)
	}
	return &chainedConn{Conn: vnc, closers: []func() error{hop2.Close, hop1.Close}}, nil
}

// SerialTerminal 是一路串行控制台终端：读端为串口输出，写端为键入；
// Resize 调整代理侧 PTY 尺寸（guest 内 tty 需另行 stty 同步）。
type SerialTerminal struct {
	io.Reader
	io.Writer
	session *ssh.Session
	closers []func() error
}

func (t *SerialTerminal) Resize(rows, cols int) error {
	return t.session.WindowChange(rows, cols)
}

func (t *SerialTerminal) Close() error {
	err := t.session.Close()
	for _, fn := range t.closers {
		_ = fn()
	}
	return err
}

// OpenSerial 经第二跳打开交互式 PTY 会话，会话的输入输出即串口字节流。
func (sess *ConsoleSession) OpenSerial(ctx context.Context, rows, cols int) (*SerialTerminal, error) {
	hop1, hop2, err := sess.dialHops()
	if err != nil {
		return nil, err
	}
	term, err := openSerialSession(hop2, rows, cols)
	if err != nil {
		hop2.Close()
		hop1.Close()
		return nil, err
	}
	term.closers = []func() error{hop2.Close, hop1.Close}
	return term, nil
}

// openSerialSession 在第二跳上申请 PTY 并启动 shell（即串口流）。
func openSerialSession(hop2 *ssh.Client, rows, cols int) (*SerialTerminal, error) {
	session, err := hop2.NewSession()
	if err != nil {
		return nil, fmt.Errorf("open serial session: %w", err)
	}
	modes := ssh.TerminalModes{ssh.ECHO: 1, ssh.TTY_OP_ISPEED: 115200, ssh.TTY_OP_OSPEED: 115200}
	if err := session.RequestPty("xterm-256color", rows, cols, modes); err != nil {
		session.Close()
		return nil, fmt.Errorf("request pty: %w", err)
	}
	stdin, err := session.StdinPipe()
	if err != nil {
		session.Close()
		return nil, fmt.Errorf("serial stdin pipe: %w", err)
	}
	stdout, err := session.StdoutPipe()
	if err != nil {
		session.Close()
		return nil, fmt.Errorf("serial stdout pipe: %w", err)
	}
	if err := session.Shell(); err != nil {
		session.Close()
		return nil, fmt.Errorf("start serial shell: %w", err)
	}
	return &SerialTerminal{Reader: stdout, Writer: stdin, session: session}, nil
}

// chainedConn 关闭数据流时连带关闭底层 SSH 连接，避免隧道泄漏。
type chainedConn struct {
	net.Conn
	closers []func() error
}

func (c *chainedConn) Close() error {
	err := c.Conn.Close()
	for _, fn := range c.closers {
		_ = fn()
	}
	return err
}
