// Copyright 2016 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package recipe

import (
	"sync"
	"time"

	"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
	"github.com/coreos/etcd/clientv3"
	pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
	"github.com/coreos/etcd/lease"
)

// only keep one ephemeral lease per clientection
var clientLeases clientLeaseMgr = clientLeaseMgr{leases: make(map[*clientv3.Client]*leaseKeepAlive)}

type clientLeaseMgr struct {
	leases map[*clientv3.Client]*leaseKeepAlive
	mu     sync.Mutex
}

type leaseKeepAlive struct {
	id    lease.LeaseID
	donec chan struct{}
}

func SessionLease(client *clientv3.Client) (lease.LeaseID, error) {
	return clientLeases.sessionLease(client, 120)
}

func SessionLeaseTTL(client *clientv3.Client, ttl int64) (lease.LeaseID, error) {
	return clientLeases.sessionLease(client, ttl)
}

// StopSessionLease ends the refresh for the session lease. This is useful
// in case the state of the client clientection is indeterminate (revoke
// would fail) or if transferring lease ownership.
func StopSessionLease(client *clientv3.Client) {
	clientLeases.mu.Lock()
	lka, ok := clientLeases.leases[client]
	if ok {
		delete(clientLeases.leases, client)
	}
	clientLeases.mu.Unlock()
	if lka != nil {
		lka.donec <- struct{}{}
		<-lka.donec
	}
}

// RevokeSessionLease revokes the session lease.
func RevokeSessionLease(client *clientv3.Client) (err error) {
	clientLeases.mu.Lock()
	lka := clientLeases.leases[client]
	clientLeases.mu.Unlock()
	StopSessionLease(client)
	if lka != nil {
		req := &pb.LeaseRevokeRequest{ID: int64(lka.id)}
		_, err = client.Lease.LeaseRevoke(context.TODO(), req)
	}
	return err
}

func (clm *clientLeaseMgr) sessionLease(client *clientv3.Client, ttl int64) (lease.LeaseID, error) {
	clm.mu.Lock()
	defer clm.mu.Unlock()
	if lka, ok := clm.leases[client]; ok {
		return lka.id, nil
	}

	resp, err := client.Lease.LeaseCreate(context.TODO(), &pb.LeaseCreateRequest{TTL: ttl})
	if err != nil {
		return lease.NoLease, err
	}
	id := lease.LeaseID(resp.ID)

	ctx, cancel := context.WithCancel(context.Background())
	keepAlive, err := client.Lease.LeaseKeepAlive(ctx)
	if err != nil || keepAlive == nil {
		return lease.NoLease, err
	}

	lka := &leaseKeepAlive{id: id, donec: make(chan struct{})}
	clm.leases[client] = lka

	// keep the lease alive until clientection error
	go func() {
		defer func() {
			keepAlive.CloseSend()
			clm.mu.Lock()
			delete(clm.leases, client)
			clm.mu.Unlock()
			cancel()
			close(lka.donec)
		}()

		ttl := resp.TTL
		for {
			lreq := &pb.LeaseKeepAliveRequest{ID: int64(id)}
			select {
			case <-lka.donec:
				return
			case <-time.After(time.Duration(ttl/2) * time.Second):
			}
			if err := keepAlive.Send(lreq); err != nil {
				break
			}
			resp, err := keepAlive.Recv()
			if err != nil {
				break
			}
			ttl = resp.TTL
		}
	}()

	return id, nil
}
