-- Advanced Damage System - public, read-only machine health facade.
-- Contract v1 exposes detached health facts separately from diagnostic visibility.

ADS_MachineHealthReadFacade = ADS_MachineHealthReadFacade or {}
ADS_MachineHealthReadFacade.CONTRACT_VERSION = 1
ADS_MachineHealthReadFacade.REVIEW_BUILD_ID = "fm-20260726-machine-health-mh4"
ADS_MachineHealthReadFacade._vehicleState = ADS_MachineHealthReadFacade._vehicleState
    or setmetatable({}, {__mode="k"})
ADS_MachineHealthReadFacade._identityState = ADS_MachineHealthReadFacade._identityState or {}
ADS_MachineHealthReadFacade._reviewIdentity = ADS_MachineHealthReadFacade._reviewIdentity
ADS_MachineHealthReadFacade._reviewLogged = ADS_MachineHealthReadFacade._reviewLogged or {}

MessageType = MessageType or {}
if MessageType.ADS_MACHINE_HEALTH_CHANGED == nil and nextMessageTypeId ~= nil then
    MessageType.ADS_MACHINE_HEALTH_CHANGED = nextMessageTypeId()
end

local SEVERITY_RANK = {unknown=0,minor=1,moderate=2,major=3,critical=4}

local function percent(value)
    value=tonumber(value)
    if value==nil then return nil end
    return math.max(0,math.min(100,math.floor(value*100+0.5)))
end

local function localized(key)
    if type(key)~="string"or key==""or g_i18n==nil or g_i18n.getText==nil then return nil end
    if g_i18n.hasText~=nil and not g_i18n:hasText(key)then return nil end
    local ok,text=pcall(g_i18n.getText,g_i18n,key)
    return ok and type(text)=="string"and text~=""and text~=key and text or nil
end

local function normalizeSeverity(value)
    local text=string.lower(tostring(value or""))
    if string.find(text,"critical",1,true)or string.find(text,"shutdown",1,true)
        or string.find(text,"permanent",1,true)then return"critical"end
    if string.find(text,"major",1,true)then return"major"end
    if string.find(text,"moderate",1,true)or string.find(text,"reduce_power",1,true)then
        return"moderate"
    end
    if string.find(text,"minor",1,true)then return"minor"end
    return"unknown"
end

local function getDefinition(faultTypeId)
    local registry=ADS_Breakdowns~=nil and ADS_Breakdowns.BreakdownRegistry or nil
    return type(registry)=="table"and registry[faultTypeId]or nil
end

local function getStage(definition,index)
    local stages=type(definition)=="table"and definition.stages or nil
    return type(stages)=="table"and stages[math.max(1,math.floor(tonumber(index)or 1))]or nil
end

local function getEffectFacts(stage)
    local result={blocksManual=false,blocksAi=false,messageKey=nil,hasEffects=false}
    for _,effect in ipairs(type(stage)=="table"and type(stage.effects)=="table"and stage.effects or{})do
        result.hasEffects=true
        local data=type(effect)=="table"and effect.extraData or nil
        if type(data)=="table"then
            result.blocksManual=result.blocksManual or data.blocksManualOperation==true
            result.blocksAi=result.blocksAi or data.disableAi==true
            if result.messageKey==nil and type(data.message)=="string"and data.message~=""then
                result.messageKey=data.message
            end
        end
    end
    return result
end

local function classifyBreakdown(faultTypeId,breakdown)
    local definition=getDefinition(faultTypeId)
    local stage=getStage(definition,type(breakdown)=="table"and breakdown.stage or 1)
    local effects=getEffectFacts(stage)
    local active=type(breakdown)=="table"and breakdown.isActive~=false
    local diagnosed=type(breakdown)=="table"and breakdown.isVisible==true
    local selectable=type(definition)=="table"and definition.isSelectable==true
    local validStage=type(stage)=="table"
    local suspended=not active and type(breakdown)=="table"
        and (tonumber(breakdown.resumeTimer)or 0)>0
    local healthRelevant=active and validStage and(selectable or effects.hasEffects)
    return{
        definition=definition,stage=stage,effects=effects,active=active,
        resolved=not active and not suspended,suspended=suspended,
        diagnosed=diagnosed,discovered=diagnosed,selectable=selectable,
        validStage=validStage,healthRelevant=healthRelevant,
        reportVisible=healthRelevant and diagnosed and selectable
    }
end

local function projectFault(faultTypeId,breakdown,classification)
    local definition=classification.definition
    local stage=classification.stage
    local effects=classification.effects
    local stageIndex=math.max(1,math.floor(tonumber(type(breakdown)=="table"and breakdown.stage)or 1))
    local severity=normalizeSeverity(type(stage)=="table"and stage.severity or nil)
    local blocksManual=effects.blocksManual
        or(type(stage)=="table"and stage.blocksManualOperation==true)
        or(type(definition)=="table"and definition.blocksManualOperation==true)
    local blocksAi=effects.blocksAi
        or(type(stage)=="table"and stage.blocksAiOperation==true)
        or(type(definition)=="table"and definition.blocksAiOperation==true)
    local impact="degraded"
    if blocksManual then impact="blocked"
    elseif blocksAi or severity=="critical"or severity=="major"then impact="limited"
    elseif severity=="unknown"then impact="unknown"end
    local title=localized(effects.messageKey)
    -- ADS' live warning path presents effect messages before inspection. A title
    -- is therefore owner-public when the current stage exposes such a message.
    local titleAvailable=title~=nil and effects.messageKey~=nil
    return{
        faultTypeId=tostring(faultTypeId),stage=stageIndex,
        active=classification.active==true,resolved=classification.resolved==true,
        suspended=classification.suspended==true,
        diagnosed=classification.diagnosed==true,discovered=classification.discovered==true,
        reportVisible=classification.reportVisible==true,
        workshopVisible=classification.reportVisible==true,
        titleAvailable=titleAvailable,title=titleAvailable and title or nil,
        titleKey=titleAvailable and effects.messageKey or nil,
        known=classification.diagnosed==true or titleAvailable,
        severity=severity,operationalImpact=impact,
        blocksManualOperation=blocksManual==true,blocksAiOperation=blocksAi==true,
        repairRequired=true
    }
end

local function faultRank(fault)
    return(fault.blocksManualOperation and 100000 or 0)
        +(SEVERITY_RANK[fault.severity]or 0)*1000
        +(fault.repairRequired and 100 or 0)+(tonumber(fault.stage)or 0)
end

local function sortFaults(faults)
    table.sort(faults,function(a,b)
        local ar,br=faultRank(a),faultRank(b)
        return ar~=br and ar>br or(ar==br and a.faultTypeId<b.faultTypeId)
    end)
end

local function getBreakdownProjection(vehicle)
    local spec=vehicle~=nil and vehicle.spec_AdvancedDamageSystem or nil
    local healthFaults,diagnosedFaults={},{}
    local counts={raw=0,active=0,diagnosed=0,reportVisible=0,inactive=0,
        suspended=0,unselectable=0,invalidStage=0}
    for faultTypeId,breakdown in pairs(type(spec)=="table"
        and type(spec.activeBreakdowns)=="table"and spec.activeBreakdowns or{})do
        counts.raw=counts.raw+1
        local classification=classifyBreakdown(faultTypeId,breakdown)
        if classification.healthRelevant then
            local fault=projectFault(faultTypeId,breakdown,classification)
            healthFaults[#healthFaults+1]=fault
            counts.active=counts.active+1
            if classification.diagnosed then counts.diagnosed=counts.diagnosed+1 end
            if classification.reportVisible then
                diagnosedFaults[#diagnosedFaults+1]=fault
                counts.reportVisible=counts.reportVisible+1
            end
            if not classification.selectable then counts.unselectable=counts.unselectable+1 end
        elseif classification.suspended then counts.suspended=counts.suspended+1
        elseif not classification.active then counts.inactive=counts.inactive+1
        else counts.invalidStage=counts.invalidStage+1 end
    end
    sortFaults(healthFaults)
    sortFaults(diagnosedFaults)
    return healthFaults,diagnosedFaults,counts
end

local function buildSnapshot(vehicle)
    local spec=vehicle~=nil and vehicle.spec_AdvancedDamageSystem or nil
    if type(spec)~="table"or spec.isExcludedVehicle==true then
        return{schemaVersion=1,available=false,complete=false,revision=0,
            faults={activeCount=0,count=0,primary=nil,items={}},
            diagnostics={diagnosedCount=0,reportVisibleCount=0,primaryDiagnosed=nil}}
    end
    local currentCondition=vehicle.getConditionLevel~=nil and vehicle:getConditionLevel()or spec.conditionLevel
    local currentService=vehicle.getServiceLevel~=nil and vehicle:getServiceLevel()or spec.serviceLevel
    local inspectedCondition,inspectionComplete=nil,false
    if vehicle.getLastInspectedCondition~=nil then
        inspectedCondition,inspectionComplete=vehicle:getLastInspectedCondition()
    end
    local faults,diagnosedFaults,counts=getBreakdownProjection(vehicle)
    local threshold=ADS_Config~=nil and ADS_Config.CORE~=nil
        and tonumber(ADS_Config.CORE.SERVICE_EXPIRED_THRESHOLD)or 0.5
    local serviceLevel=tonumber(currentService)
    local due=serviceLevel~=nil and serviceLevel<=threshold
    local remaining=serviceLevel~=nil and ADS_Utils~=nil
        and ADS_Utils.getServiceIntervalRemainingRatio~=nil
        and ADS_Utils.getServiceIntervalRemainingRatio(serviceLevel)or nil
    local readyState=AdvancedDamageSystem~=nil and AdvancedDamageSystem.STATUS~=nil
        and AdvancedDamageSystem.STATUS.READY or"ads_spec_state_ready"
    return{
        schemaVersion=1,available=true,complete=true,
        condition={currentPercent=percent(currentCondition),inspectedPercent=percent(inspectedCondition),
            inspectionComplete=inspectionComplete==true},
        service={currentPercent=percent(currentService),due=due==true,
            soon=due~=true and remaining~=nil and remaining<=0.25,
            underService=spec.currentState~=nil and spec.currentState~=readyState},
        faults={activeCount=#faults,count=#faults,primary=faults[1],items=faults},
        diagnostics={diagnosedCount=counts.diagnosed,reportVisibleCount=#diagnosedFaults,
            primaryDiagnosed=diagnosedFaults[1]},
        reviewProjectionCounts=counts
    }
end

local function fingerprint(snapshot)
    local parts={tostring(snapshot.available),tostring(snapshot.complete),
        tostring(snapshot.condition and snapshot.condition.currentPercent),
        tostring(snapshot.condition and snapshot.condition.inspectedPercent),
        tostring(snapshot.condition and snapshot.condition.inspectionComplete),
        tostring(snapshot.service and snapshot.service.currentPercent),
        tostring(snapshot.service and snapshot.service.due),
        tostring(snapshot.service and snapshot.service.soon),
        tostring(snapshot.service and snapshot.service.underService)}
    for _,fault in ipairs(snapshot.faults and snapshot.faults.items or{})do
        parts[#parts+1]=table.concat({fault.faultTypeId,fault.stage,fault.severity,
            fault.operationalImpact,tostring(fault.active),tostring(fault.diagnosed),
            tostring(fault.reportVisible),tostring(fault.titleAvailable),
            tostring(fault.blocksManualOperation),tostring(fault.blocksAiOperation),
            tostring(fault.repairRequired),tostring(fault.titleKey or"")},":")
    end
    return table.concat(parts,"|")
end

local function vehicleIdentity(vehicle)
    if vehicle==nil then return nil end
    if vehicle.getUniqueId~=nil then
        local ok,value=pcall(vehicle.getUniqueId,vehicle)
        if ok and value~=nil then return tostring(value)end
    end
    return tostring(vehicle.uniqueId or vehicle.id or vehicle.rootNode or"")
end

local function stateFromSnapshot(snapshot,revision,token)
    return{revision=revision,fingerprint=token,
        faultCount=tonumber(snapshot.faults and snapshot.faults.activeCount)or 0,
        diagnosedCount=tonumber(snapshot.diagnostics and snapshot.diagnostics.diagnosedCount)or 0,
        reportVisibleCount=tonumber(snapshot.diagnostics and snapshot.diagnostics.reportVisibleCount)or 0,
        primaryFaultId=snapshot.faults and snapshot.faults.primary
            and snapshot.faults.primary.faultTypeId or nil}
end

function ADS_MachineHealthReadFacade:getContractVersion()return self.CONTRACT_VERSION end
function ADS_MachineHealthReadFacade:isAvailable(vehicle)
    local spec=vehicle~=nil and vehicle.spec_AdvancedDamageSystem or nil
    return type(spec)=="table"and spec.isExcludedVehicle~=true
end

function ADS_MachineHealthReadFacade:_observe(vehicle,publish)
    local snapshot=buildSnapshot(vehicle)
    local identity=vehicleIdentity(vehicle)
    local state=identity~=nil and identity~=""and self._identityState[identity]
        or self._vehicleState[vehicle]
    local token=fingerprint(snapshot)
    if state==nil then
        state=stateFromSnapshot(snapshot,1,token)
    elseif state.fingerprint~=token then
        local previousFaultCount=tonumber(state.faultCount)or 0
        local previousDiagnosedCount=tonumber(state.diagnosedCount)or 0
        state=stateFromSnapshot(snapshot,(tonumber(state.revision)or 0)+1,token)
        local isTarget=self._reviewIdentity~=nil
            and tostring(self._reviewIdentity)==tostring(identity)
        if isTarget and previousFaultCount>0 and state.faultCount>0
            and previousDiagnosedCount==0 and state.diagnosedCount>0
            and self._reviewLogged.afterInspection~=true then
            print(string.format(
                "[PMSMachineHealthDiag] buildId=%s phase=afterInspection stableId=%s ownerRevision=%d activeCount=%d diagnosedCount=%d reportVisibleCount=%d primaryHealthFaultId=%s primaryDiagnosedFaultId=%s severity=%s titleAvailable=%s",
                self.REVIEW_BUILD_ID,tostring(identity),state.revision,state.faultCount,
                state.diagnosedCount,state.reportVisibleCount,tostring(state.primaryFaultId),
                tostring(snapshot.diagnostics and snapshot.diagnostics.primaryDiagnosed
                    and snapshot.diagnostics.primaryDiagnosed.faultTypeId or nil),
                tostring(snapshot.faults and snapshot.faults.primary
                    and snapshot.faults.primary.severity or"unknown"),
                tostring(snapshot.faults and snapshot.faults.primary
                    and snapshot.faults.primary.titleAvailable==true)))
            self._reviewLogged.afterInspection=true
        end
        if isTarget and previousFaultCount>0 and state.faultCount==0
            and self._reviewLogged.afterRepair~=true then
            print(string.format(
                "[PMSMachineHealthDiag] buildId=%s phase=afterRepair stableId=%s ownerRevision=%d activeCount=0 diagnosedCount=0 primaryHealthFaultId=nil summaryState=pendingPmsRefresh taskExists=pending advisorObservationExists=pending",
                self.REVIEW_BUILD_ID,tostring(identity),state.revision))
            self._reviewLogged.afterRepair=true
        end
        if publish==true and g_messageCenter~=nil and MessageType.ADS_MACHINE_HEALTH_CHANGED~=nil then
            g_messageCenter:publish(MessageType.ADS_MACHINE_HEALTH_CHANGED,identity,state.revision)
        end
    end
    self._vehicleState[vehicle]=state
    if identity~=nil and identity~=""then self._identityState[identity]=state end
    snapshot.revision=state.revision
    return snapshot
end

function ADS_MachineHealthReadFacade:getVehicleHealthRevision(vehicle)
    return self:_observe(vehicle,false).revision
end
function ADS_MachineHealthReadFacade:getVehicleHealth(vehicle)return self:_observe(vehicle,false)end
function ADS_MachineHealthReadFacade:notifyVehicleChanged(vehicle)return self:_observe(vehicle,true)end

function ADS_MachineHealthReadFacade:setReviewDiagnosticIdentity(identity)
    identity=tostring(identity or"")
    if identity==""then return end
    self._reviewIdentity=identity
    local state=self._identityState[identity]
    if state~=nil and state.faultCount>0 and self._reviewLogged.faultCreated~=true then
        print(string.format(
            "[PMSMachineHealthDiag] buildId=%s phase=faultCreated stableId=%s activeCount=%d diagnosedCount=%d reportVisibleCount=%d primaryHealthFaultId=%s primaryDiagnosedFaultId=%s severity=ownerProjected titleAvailable=ownerProjected",
            self.REVIEW_BUILD_ID,identity,state.faultCount,state.diagnosedCount,
            state.reportVisibleCount,tostring(state.primaryFaultId),
            state.diagnosedCount>0 and tostring(state.primaryFaultId)or"nil"))
        self._reviewLogged.faultCreated=true
    end
end

function ADS_MachineHealthReadFacade:reviewOwnerRepairCompleted(vehicle,serviceType)
    local status=AdvancedDamageSystem~=nil and AdvancedDamageSystem.STATUS or{}
    if serviceType~=status.REPAIR and serviceType~=status.OVERHAUL then return end
    self:_observe(vehicle,false)
end

function ADS_MachineHealthReadFacade:reviewReportState(vehicle,reportFaultCount,condition,service)
    local identity=vehicleIdentity(vehicle)
    if self._reviewIdentity==nil or tostring(identity)~=tostring(self._reviewIdentity)
        or self._reviewLogged.reportState==true then return end
    local snapshot=self:_observe(vehicle,false)
    print(string.format(
        "[PMSMachineHealthDiag] buildId=%s phase=reportState stableId=%s activeCount=%s diagnosedCount=%s reportVisibleCount=%s reportFaultCount=%s condition=%s service=%s",
        self.REVIEW_BUILD_ID,tostring(identity),
        tostring(snapshot.faults and snapshot.faults.activeCount or 0),
        tostring(snapshot.diagnostics and snapshot.diagnostics.diagnosedCount or 0),
        tostring(snapshot.diagnostics and snapshot.diagnostics.reportVisibleCount or 0),
        tostring(reportFaultCount or 0),tostring(condition),tostring(service)))
    self._reviewLogged.reportState=true
end

function ADS_MachineHealthReadFacade:forgetVehicle(vehicle)
    local identity=vehicleIdentity(vehicle)
    self._vehicleState[vehicle]=nil
    if identity~=nil and identity~=""then self._identityState[identity]=nil end
end
function ADS_MachineHealthReadFacade:reset()
    self._vehicleState=setmetatable({}, {__mode="k"})
    self._identityState={}
    self._reviewIdentity=nil
    self._reviewLogged={}
end

g_globalMods=g_globalMods or{}
g_globalMods.ADSMachineHealthReadFacade=ADS_MachineHealthReadFacade
