// Copyright 2016 Martin Hebnes Pedersen (LA5NTA). All rights reserved.
// Use of this source code is governed by the MIT-license that can be
// found in the LICENSE file.

package app

import (
	"context"
	"fmt"
	"log"
	"net"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/la5nta/pat/api/types"
	"github.com/la5nta/pat/internal/buildinfo"

	"github.com/la5nta/wl2k-go/fbb"
)

type ex struct {
	conn   net.Conn
	target string
	master bool
	errors chan error
}

func (a *App) exchangeLoop(ctx context.Context) chan ex {
	ce := make(chan ex)
	go func() {
		for {
			select {
			case ex := <-ce:
				ex.errors <- a.sessionExchange(ex.conn, ex.target, ex.master)
				close(ex.errors)
			case <-ctx.Done():
				return
			}
		}
	}()
	return ce
}

func (a *App) exchange(conn net.Conn, targetCall string, master bool) error {
	e := ex{
		conn:   conn,
		target: targetCall,
		master: master,
		errors: make(chan error),
	}
	a.exchangeChan <- e
	return <-e.errors
}

type NotifyMBox struct {
	fbb.MBoxHandler
	*App
}

func (m NotifyMBox) ProcessInbound(msgs ...*fbb.Message) error {
	if err := m.MBoxHandler.ProcessInbound(msgs...); err != nil {
		return err
	}
	for _, msg := range msgs {
		m.websocketHub.WriteNotification(types.Notification{
			Title: fmt.Sprintf("New message from %s", msg.From().Addr),
			Body:  msg.Subject(),
		})
		if isServiceMessage(msg) {
			m.onServiceMessageReceived(msg)
		}
	}
	return nil
}

func (m NotifyMBox) GetInboundAnswers(p []fbb.Proposal) []fbb.ProposalAnswer {
	answers := make([]fbb.ProposalAnswer, len(p))
	var outsideLimit bool
	var hasAccountActivation bool
	for idx, p := range p {
		answers[idx] = m.GetInboundAnswer(p)
		outsideLimit = outsideLimit || p.CompressedSize() >= m.config.AutoDownloadSizeLimit
		if pm := p.PendingMessage(); pm != nil {
			hasAccountActivation = hasAccountActivation || isAccountActivation(pm.From, pm.Subject)
		}
	}
	if hasAccountActivation {
		res := <-m.promptHub.Prompt(
			context.Background(),
			time.Minute,
			types.PromptKindAccountActivation,
			"Important: Your New Account Password",
		)
		if declined := res.Value != "accept"; declined {
			// Defer all proposals
			for idx := range answers {
				answers[idx] = fbb.Defer
			}
			return answers
		}
	}
	if !outsideLimit || m.config.AutoDownloadSizeLimit < 0 {
		// All proposals are within the prompt limit. Go ahead.
		return answers
	}

	// Build multi-select build options for those accepted by the mailbox handler.
	var options []PromptOption
	for idx, p := range p {
		if answers[idx] != fbb.Accept {
			continue
		}
		answers[idx] = fbb.Defer // Defer unless user explicitly accepts through prompt answer.
		sender, subject := "Unkown sender", "Unknown subject"
		if pm := p.PendingMessage(); pm != nil {
			sender, subject = pm.From.String(), pm.Subject
		}
		desc := fmt.Sprintf("%s (%d bytes): %s", sender, p.CompressedSize(), subject)
		options = append(options, PromptOption{Value: p.MID(), Desc: desc, Checked: p.CompressedSize() < m.config.AutoDownloadSizeLimit})
	}

	// Prompt the user
	ans := <-m.promptHub.Prompt(context.Background(), time.Minute, PromptKindMultiSelect, "Select messages for download", options...)

	// If timeout was reached, use our default values to fill in for the user
	if ans.Err == context.DeadlineExceeded {
		var checked []string
		for _, opt := range options {
			if opt.Checked {
				checked = append(checked, opt.Value)
			}
		}
		ans.Value = strings.Join(checked, ",")
	}

	// For each mid in answer, search the proposals and update answer to Accept.
	for _, val := range strings.Split(ans.Value, ",") {
		for idx, p := range p {
			if p.MID() != val {
				continue
			}
			answers[idx] = fbb.Accept
		}
	}

	return answers
}

func (a *App) sessionExchange(conn net.Conn, targetCall string, master bool) error {
	a.exchangeConn = conn
	a.websocketHub.UpdateStatus()
	defer func() { a.exchangeConn = nil; a.websocketHub.UpdateStatus() }()

	// New wl2k Session
	targetCall = strings.Split(targetCall, ` `)[0]
	session := fbb.NewSession(
		a.options.MyCall,
		targetCall,
		a.config.Locator,
		NotifyMBox{a.mbox, a},
	)

	session.SetUserAgent(fbb.UserAgent{
		Name:    buildinfo.AppName,
		Version: buildinfo.Version,
	})

	if len(a.config.MOTD) > 0 {
		session.SetMOTD(a.config.MOTD...)
	}

	// Handle secure login
	session.SetSecureLoginHandleFunc(func(addr fbb.Address) (string, error) {
		if addr.Addr == a.options.MyCall && a.config.SecureLoginPassword != "" {
			return a.config.SecureLoginPassword, nil
		}
		for _, aux := range a.config.AuxAddrs {
			if !addr.EqualString(aux.Address) {
				continue
			}
			switch {
			case aux.Password != nil:
				return *aux.Password, nil
			case a.config.SecureLoginPassword != "":
				return a.config.SecureLoginPassword, nil
			}
		}
		resp := <-a.promptHub.Prompt(context.Background(), time.Minute, PromptKindPassword, "Enter secure login password for "+addr.String())
		return resp.Value, resp.Err
	})

	for _, addr := range a.config.AuxAddrs {
		session.AddAuxiliaryAddress(fbb.AddressFromString(addr.Address))
	}

	session.IsMaster(master)
	session.SetLogger(log.New(a.termWriter, "", 0))

	session.SetStatusUpdater(StatusUpdate{a.websocketHub})

	if a.options.Robust {
		session.SetRobustMode(fbb.RobustForced)
	}

	log.Printf("Connected to %s (%s)", conn.RemoteAddr(), conn.RemoteAddr().Network())

	start := time.Now()

	stats, err := session.Exchange(conn)
	if fbb.IsLoginFailure(err) {
		fmt.Println("NOTE: A new password scheme for Winlink is being implemented as of 2018-01-31.")
		fmt.Println("      Users with passwords created/changed prior to January 31, 2018 should be")
		fmt.Println("      aware that their password MUST be entered in ALL-UPPERCASE letters. Only")
		fmt.Println("      passwords created/changed/issued after January 31, 2018 should/may contain")
		fmt.Println("      lowercase letters. - https://github.com/la5nta/pat/issues/113")
	}

	if t, _ := strconv.ParseBool(os.Getenv("PAT_MOCK_NEW_ACCOUNT_MSG")); t {
		log.Println("Mocking new account msg...")
		NotifyMBox{a.mbox, a}.ProcessInbound(mockNewAccountMsg())
	}

	event := map[string]interface{}{
		"mycall":              session.Mycall(),
		"targetcall":          session.Targetcall(),
		"remote_fw":           session.RemoteForwarders(),
		"remote_sid":          session.RemoteSID(),
		"master":              master,
		"local_locator":       a.config.Locator,
		"auxiliary_addresses": a.config.AuxAddrs,
		"network":             conn.RemoteAddr().Network(),
		"remote_addr":         conn.RemoteAddr().String(),
		"local_addr":          conn.LocalAddr().String(),
		"sent":                stats.Sent,
		"received":            stats.Received,
		"start":               start.Unix(),
		"end":                 time.Now().Unix(),
		"success":             err == nil,
	}
	if err != nil {
		event["error"] = err.Error()
	}

	a.eventLog.Log("exchange", event)

	return err
}

func (a *App) AbortActiveConnection(dirty bool) (ok bool) {
	switch {
	case dirty:
		// This mean we've already tried to abort, but the connection is still active.
		// Fallback to the below cases to try to identify the busy modem and abort hard.
	case a.dialing != nil:
		// If we're currently dialing a transport, attempt to abort by cancelling the associated context.
		log.Printf("Got abort signal while dialing %s, cancelling...", a.dialing.Scheme)
		go a.dialCancelFunc()
		return true
	case a.exchangeConn != nil:
		// If we have an active connection, close it gracefully.
		log.Println("Got abort signal, disconnecting...")
		go a.exchangeConn.Close()
		return true
	}

	// Any connection and/or dial operation has been cancelled at this point.
	// User is attempting to abort something, so try to identify any non-idling transports and abort.
	// It might be a "dirty disconnect" of an already cancelled connection or dial operation which is in the
	// process of gracefully terminating. It might also be an attempt to close an inbound P2P connection.
	switch {
	case a.ardop != nil && !a.ardop.Idle():
		if dirty {
			log.Println("Dirty disconnecting ardop...")
			a.ardop.Abort()
			return true
		}
		log.Println("Disconnecting ardop...")
		go func() {
			if err := a.ardop.Disconnect(); err != nil {
				log.Println(err)
			}
		}()
		return true
	case a.varaFM != nil && !a.varaFM.Idle():
		if dirty {
			log.Println("Dirty disconnecting varafm...")
			a.varaFM.Abort()
			return true
		}
		log.Println("Disconnecting varafm...")
		go func() {
			if err := a.varaFM.Close(); err != nil {
				log.Println(err)
			}
		}()
		return true
	case a.varaHF != nil && !a.varaHF.Idle():
		if dirty {
			log.Println("Dirty disconnecting varahf...")
			a.varaHF.Abort()
			return true
		}
		log.Println("Disconnecting varahf...")
		go func() {
			if err := a.varaHF.Close(); err != nil {
				log.Println(err)
			}
		}()
		return true
	case a.pactor != nil:
		log.Println("Disconnecting pactor...")
		err := a.pactor.Close()
		if err != nil {
			log.Println(err)
		}
		return err == nil
	default:
		return false
	}
}

type StatusUpdate struct{ WSHub }

func (s StatusUpdate) UpdateStatus(stat fbb.Status) {
	var prop fbb.Proposal
	switch {
	case stat.Receiving != nil:
		prop = *stat.Receiving
	case stat.Sending != nil:
		prop = *stat.Sending
	}

	s.WriteProgress(types.Progress{
		MID:              prop.MID(),
		BytesTotal:       stat.BytesTotal,
		BytesTransferred: stat.BytesTransferred,
		Subject:          prop.Title(),
		Receiving:        stat.Receiving != nil,
		Sending:          stat.Sending != nil,
		Done:             stat.Done,
	})

	percent := float64(stat.BytesTransferred) / float64(stat.BytesTotal) * 100
	fmt.Printf("\r%s: %3.0f%%", prop.Title(), percent)

	if stat.Done {
		fmt.Println("")
	}
	os.Stdout.Sync()
}
