# validation is used by other modules
validate = require('json-schema').validate

@db = db =
    main: require('dirty') '/tmp/openvpn.db'
    user: require('dirty') '/tmp/openvpnusers.db'

db.user.on 'load', ->
    console.log 'loaded openvpnusers.db'
    db.user.forEach (key,val) ->
        console.log 'found ' + key

@lookup = lookup = (id) ->
    console.log "looking up user ID: #{id}"
    entry = db.user.get id
    if entry

        if userschema?
            console.log 'performing schema validation on retrieved user entry'
            result = validate entry, userschema
            console.log result
            return new Error "Invalid user retrieved: #{result.errors}" unless result.valid

        return entry
    else
        return new Error "No such user ID: #{id}"

@userschema = userschema =
        name: "openvpn"
        type: "object"
        additionalProperties: false
        properties:
            id:    { type: "string", required: true }
            email: { type: "string", required: true }
            push:
                items: { type: "string" }

@include = ->

    webreq = require 'request'
    fs = require 'fs'
    path = require 'path'
    exec = require('child_process').exec

    # validation is used by other modules
    validate = require('json-schema').validate

    services = require './services'

    # testing openvpn validation with test schema
    schema =
        name: "openvpn"
        type: "object"
        additionalProperties: false
        properties:
            port:                {"type":"number", "required":true}
            dev:                 {"type":"string", "required":true}
            proto:               {"type":"string", "required":true}
            ca:                  {"type":"string", "required":true}
            dh:                  {"type":"string", "required":true}
            cert:                {"type":"string", "required":true}
            key:                 {"type":"string", "required":true}
            server:              {"type":"string", "required":true}
            'script-security':   {"type":"string", "required":false}
            multihome:           {"type":"boolean", "required":false}
            management:          {"type":"string", "required":false}
            cipher:              {"type":"string", "required":false}
            'tls-cipher':        {"type":"string", "required":false}
            auth:                {"type":"string", "required":false}
            topology:            {"type":"string", "required":false}
            'route-gateway':     {"type":"string", "required":false}
            'client-config-dir': {"type":"string", "required":false}
            'ccd-exclusive':     {"type":"boolean", "required":false}
            'client-to-client':  {"type":"boolean", "required":false}
            route:
                items: { type: "string" }
            push:
                items: { type: "string" }
            'max-clients':       {"type":"number", "required":false}
            'persist-key':       {"type":"boolean", "required":false}
            'persist-tun':       {"type":"boolean", "required":false}
            status:              {"type":"string", "required":false}
            keepalive:           {"type":"string", "required":false}
            'comp-lzo':          {"type":"string", "required":false}
            sndbuf:              {"type":"number", "required":false}
            rcvbuf:              {"type":"number", "required":false}
            txqueuelen:          {"type":"number", "required":false}
            'replay-window':     {"type":"string", "required":false}
            verb:                {"type":"number", "required":false}
            mlock:               {"type":"boolean", "required":false}

    validateOpenvpn = ->
        console.log 'performing schema validation on incoming OpenVPN JSON'
        result = validate @body, schema
        console.log result
        return @next new Error "Invalid service openvpn posting!: #{result.errors}" unless result.valid
        @next()

    # helper routine for retrieving service data from dirty db
    loadService = ->
        result = services.lookup @params.id
        unless result instanceof Error
            @request.service = result
            @next()
        else
            return @next result

    # helper routine for retrieving service data from dirty db
    loadOpenVPN = ->
        result = db.main.get @params.id
        unless result instanceof Error
            @request.service = result
            @next()
        else
            return @next result

    @post '/services/:id/openvpn', loadService, validateOpenvpn, ->
        service = @request.service
        config = ''
        for key, val of @body
            switch (typeof val)
                when "object"
                    if val instanceof Array
                        for i in val
                            config += "#{key} #{i}\n" if key is "route"
                            config += "#{key} \"#{i}\"\n" if key is "push"
                when "number", "string"
                    config += key + ' ' + val + "\n"
                when "boolean"
                    config += key + "\n"

        #filename = __dirname+'/services/'+varguid+'/openvpn/server.conf'
        filename = '/config/openvpn/server.conf'
        try
            console.log "write openvpn config to #{filename}..."
            dir = path.dirname filename
            unless path.existsSync dir
                exec "mkdir -p #{dir}", (error, stdout, stderr) =>
                    unless error
                        fs.writeFileSync filename, config
            else
                fs.writeFileSync filename, config

            exec "touch /config/#{service.description.name}/on"

            db.main.set @params.id, @body, =>
                console.log "#{@params.id} added to OpenVPN service configuration"
                console.log @body
                return { result: true }

        catch err
            @next new Error "Unable to write configuration into #{filename}!"


    validateUser = ->
        console.log 'performing schema validation on incoming user validation JSON'
        result = validate @body, userschema
        console.log result
        return @next new Error "Invalid service openvpn posting!: #{result.errors}" unless result.valid
        @next()

    @post '/services/:id/openvpn/users', loadService, validateUser, ->
        service = @request.service
        config = ''
        for key, val of @body
            switch (typeof val)
                when "object"
                    if val instanceof Array
                        for i in val
                            config += "#{key} #{i}\n" if key is "iroute"
                            config += "#{key} \"#{i}\"\n" if key is "push"

        #filename = "/tmp/ccd/#{@body.email}"
        filename = "/config/openvpn/ccd/#{@body.email}"
        try
            console.log "write user config to #{filename}..."
            dir = path.dirname filename
            unless path.existsSync dir
                exec "mkdir -p #{dir}", (error, stdout, stderr) =>
                    unless error
                        fs.writeFileSync filename, config
            else
                fs.writeFileSync filename, config

            exec "svcs #{service.description.name} sync"

            db.user.set @body.id, @body, =>
                console.log "#{@body.email} added to OpenVPN service configuration"
                console.log @body

                @send { result: true }
        catch err
            @next new Error "Unable to write configuration into #{filename}!"

    @del '/services/:id/openvpn/users/:user', loadService, ->
        console.log @params
        userid = @params.user
        entry = db.user.get userid

        try
            throw new Error "user does not exist!" unless entry

            filename = "/config/openvpn/ccd/#{entry.email}"
            console.log "removing user config on #{filename}..."
            path.exists filename, (exists) =>
                throw new Error "user is already removed!" unless exists

                fs.unlink filename, (err) =>
                    throw err if err

                    db.user.rm userid, =>
                        console.log "removed VPN user ID: #{userid}"
                        @send { deleted: true }
        catch err
            @next new Error "Unable to remove user ID: #{userid} due to #{err}"

    @get '/services/:id/openvpn', loadService, ->
        res =
            id: @request.service.id
            users: []
            connections: []

        db.user.forEach (key,val) ->
            console.log 'found ' + key
            res.users.push val

        # TODO: should retrieve the openvpn configuration and inspect "management" and "status" property

        Lazy = require 'lazy'
        status = new Lazy
        status
            .lines
            .map(String)
            .filter (line) ->
                not (
                    /^OpenVPN/.test(line) or
                    /^Updated/.test(line) or
                    /^Common/.test(line) or
                    /^ROUTING/.test(line) or
                    /^Virtual/.test(line) or
                    /^GLOBAL/.test(line) or
                    /^UNDEF/.test(line) or
                    /^END/.test(line) or
                    /^Max bcast/.test(line))
            .map (line) ->
                #console.log "lazy: #{line}"
                return line.trim().split ','
            .forEach (fields) ->
                switch fields.length
                    when 5
                        res.connections.push {
                            cname: fields[0]
                            remote: fields[1]
                            received: fields[2]
                            sent: fields[3]
                            since: fields[4]
                        }
                    when 4
                        for conn in res.connections
                            if conn.cname is fields[1]
                                conn.ip = fields[0]
            .join =>
                console.log res
                @send res

        console.log "checking for live connections..."

        # OPENVPN MGMT API v1
        net = require 'net'
        conn = net.connect 2020, '127.0.0.1', ->
            console.log 'connection to openvpn mgmt successful!'
            response = ''
            @setEncoding 'ascii'
            @on 'prompt', =>
                @write "status\n"
            @on 'response', =>
                console.log "response: #{response}"
                status.emit 'end'
                @write "exit\n"
                @end
            @on 'data', (data) =>
                console.log "read: "+data+"\n"
                if /^>/.test(data)
                    @emit 'prompt'
                else
                    response += data
                    status.emit 'data',data
                    if /^END$/gm.test(response)
                        @emit 'response'
            @on 'end', =>
                console.log 'connection to openvpn mgmt ended!'
                status.emit 'end'
                @end

        # When we CANNOT make a connection to OPENVPN MGMT port, we fallback to checking file
        conn.on 'error', (error) ->
            console.log error
            statusfile = "/var/log/server-status.log" # hard-coded for now...

            console.log "failling back to processing #{statusfile}..."
            #statusfile = "openvpn-status.log" # hard-coded for now...
            stream = fs.createReadStream statusfile, encoding: 'utf8'
            stream.on 'open', ->
                console.log "sending #{statusfile} to lazy status..."
                stream.on 'data', (data) ->
                    status.emit 'data',data
                stream.on 'end', ->
                    status.emit 'end'

            stream.on 'error', (error) ->
                console.log error
                status.emit 'end'
