--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements.  See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You 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.
--
local Span = require('skywalking.span')
local TC = require('skywalking.tracing_context')
local Layer = require('skywalking.span_layer')
local Segment = require('skywalking.segment')
local Util = require("skywalking.util")
local Const = require('skywalking.constants')
local json = require('cjson.safe')

local metadata_shdict = ngx.shared.tracing_buffer
local ngx = ngx
local nginxComponentId = 6000
local Tracer = {}


function Tracer:start(upstream_name, correlation)
    local serviceName = metadata_shdict:get("serviceName")
    local serviceInstanceName = metadata_shdict:get('serviceInstanceName')
    local req_uri = ngx.var.uri
    if (Util.checkIgnoreSuffix(req_uri)) then
        local tracingContext =  TC.newNoOP()
        local ctx = ngx.ctx
        ctx.tracingContext = tracingContext
        ctx.entrySpan = Span.newNoOP()
        ctx.exitSpan = Span.newNoOP()
        ctx.is_finished = false
        return
    end

    local includeHostInEntrySpan = metadata_shdict:get('includeHostInEntrySpan')
    local tracingContext = TC.new(serviceName, serviceInstanceName, ngx.var.http_request_id)
    -- Constant pre-defined in SkyWalking main repo
    -- 6000 represents Nginx
    local contextCarrier = Util.tablepool_fetch("sw_contextCarrier")
    contextCarrier["sw8"] = ngx.var.http_sw8
    contextCarrier["sw8-correlation"] = ngx.var.http_sw8_correlation
    local time_now = ngx.now() * 1000
    local entrySpan
    if (includeHostInEntrySpan)  then
        entrySpan = TC.createEntrySpan(tracingContext, ngx.var.host .. req_uri, nil, contextCarrier)
    else
        entrySpan = TC.createEntrySpan(tracingContext, req_uri, nil, contextCarrier)
    end

    Span.start(entrySpan, time_now)
    Span.setComponentId(entrySpan, nginxComponentId)
    Span.setLayer(entrySpan, Layer.HTTP)

    Span.tag(entrySpan, 'http.method', ngx.req.get_method())
    Span.tag(entrySpan, 'http.params',
             ngx.var.scheme .. '://' .. ngx.var.host .. ngx.var.request_uri )

    ------------------------------------------------------
    contextCarrier = Util.tablepool_fetch("sw_contextCarrier")
    -- Use the same URI to represent incoming and forwarding requests
    -- Change it if you need.
    local exitSpan = TC.createExitSpan(tracingContext, req_uri, entrySpan)
    Span.start(exitSpan, time_now)
    Span.setComponentId(exitSpan, nginxComponentId)
    Span.setLayer(exitSpan, Layer.HTTP)

    local upstreamServerName = upstream_name
    if upstreamServerName then
        TC.inject(tracingContext, exitSpan, upstreamServerName, correlation)
    end

    -- Push the data in the context
    local ctx = ngx.ctx
    ctx.tracingContext = tracingContext
    ctx.entrySpan = entrySpan
    ctx.exitSpan = exitSpan
    ctx.is_finished = false
end

-- inject an exit span context and correlation context into carrier
-- since v1.0.0
function Tracer:inject(exitSpan, peer, correlation)
    local ctx = ngx.ctx
    local context = ctx.tracingContext

    if not context.is_noop and exitSpan ~= nil and not ctx.is_finished then
        TC.inject(context, exitSpan, peer, correlation)
    end
end

function Tracer:finish()
    -- Finish the exit span when received the first response package from upstream
    if ngx.ctx.exitSpan ~= nil and not ngx.ctx.is_finished then
        local upstream_status = tonumber(ngx.var.upstream_status)
        if upstream_status then
            Span.tag(ngx.ctx.exitSpan, 'http.status_code', upstream_status)
        end
        Span.finish(ngx.ctx.exitSpan, ngx.now() * 1000)
        ngx.ctx.exitSpan = nil
        ngx.ctx.is_finished = true
    end
end

function Tracer:prepareForReport()
    if not ngx.ctx.is_finished then
        self.finish()
    end
    local entrySpan = ngx.ctx.entrySpan
    if not entrySpan then
        return
    end

    local ngxstatus = ngx.var.status
    Span.tag(entrySpan, 'http.status_code', ngxstatus)
    if tonumber(ngxstatus) >= 500 then
        Span.errorOccurred(entrySpan)
    end

    Span.finish(entrySpan, ngx.now() * 1000)

    local ok, segment = TC.drainAfterFinished(ngx.ctx.tracingContext)
    if not ok then
        return
    end

    local segmentJson, err = json.encode(Segment.transform(segment))
    if not segmentJson then
        ngx.log(ngx.ERR, "failed to encode segment: ", err)
        return
    end
    -- ngx.log(ngx.DEBUG, 'segment = ', segmentJson)

    local length, err = metadata_shdict:lpush(Const.segment_queue, segmentJson)
    if not length then
        ngx.log(ngx.ERR, "failed to push segment: ", err)
        return
    end
    -- ngx.log(ngx.DEBUG, 'segment buffer size = ', length)

    Util.tablepool_release()
end

return Tracer
