Module:MedicoInfobox: Difference between revisions

From determinar.ia.br - Determine suas informações
No edit summary
No edit summary
 
(7 intermediate revisions by the same user not shown)
Line 1: Line 1:
-- Module:MedicoInfobox v3.3
-- FAQ (P56) e Desambiguação (P74) em divs (tags permitidas pelo Sanitizer)
-- Mapa: chamada direta via extensionTag (sem checagem no Lua — extensionTag só
-- devolve um strip marker UNIQ...QINU, nunca o HTML final, então validar dentro
-- do Lua é impossível; confiamos direto na URL já testada e aprovada)
local p = {}
local p = {}


-- ORDEM DE PRIORIDADE: aparecem primeiro, nessa ordem exata
local CFG = {
local PRIORITY_ORDER = {
     foto          = 'P1',
     'P10', -- Especialidade Médica
     nome          = 'P2',
     'P11', -- Subespecialidade
     registro      = 'P4',
     'P4', -- Número de Registro Profissional
     especialidade = 'P10',
     'P14', -- Vínculo Profissional
    subesp        = 'P11',
     'P28', -- Logradouro
    servicos      = { 'P13', 'P14', 'P18', 'P19', 'P23' },
     'P29', -- Bairro
     logradouro    = 'P28',
     'P72', -- CEP
     bairro        = 'P29',
     'P71', -- Município
     email        = 'P30',
     'P36', -- Estado
    website      = 'P31',
     'P76', -- Horário de Funcionamento
    agendamento  = 'P32',
     'P23', -- Convênio Aceito
    lat          = 'P33',
     'P19', -- Procedimento Realizado
    lon          = 'P34',
     'P18', -- Condição Tratada
     convenio      = 'P35',
     'P13', -- Área de Atuação
     estado        = 'P36',
     'P57', -- Redes Sociais
     operadora    = 'P37',
     'P39', -- Número de Estrelas no Doctoralia
     opinioes      = 'P38',
     'P40', -- Número de dúvidas Respondidas Doctoralia
     estrelas      = 'P39',
     'P60', -- Tempo de Experiência na Profissão
     duvidas      = 'P40',
     'P75', -- Google Maps (selinho especial, ver antes das publicações)
     faq          = 'P56',
     'P77', -- Publicações Científicas
     doctoralia    = 'P57',
     scholar_user  = 'P62',
    municipio    = 'P71',
    cep          = 'P72',
     bio          = 'P73',
     escopo_neg    = 'P74',
     maps_cid      = 'P75',
    horario      = 'P76',
     publicacoes  = 'P77',
    fonte        = 'P79',
    coleta        = 'P80',
}
}


-- CAMPOS TÉCNICOS: nunca aparecem no infobox (ficam só nos dados do Item)
local EXCLUIR_CHIPS = { 'unimed', 'vivest', 'seguros' }
local HIDDEN = {
    P38 = true, -- sem rótulo definido ainda — revisar antes de exibir
    P62 = true, -- Google Scholar ID
    P63 = true, -- ResearchGate Profile ID
    P64 = true, -- Doctoralia ID
    P66 = true, -- URN
    P67 = true, -- CURIE
    P68 = true, -- URI Canônica
    P70 = true, -- Token Canário
    P73 = true, -- Dentro do Escopo
    P74 = true, -- Fora do Escopo
    P78 = true, -- Arquiteto de Conhecimento
    P71 = true, -- Município (Q102 sem rótulo pt-br — ver nota anterior)
}


local IMAGE_PROPERTY = 'P1'        -- tratada à parte, vira imagem no topo
local function contemLixo( s )
local CTA_PROPERTY = 'P32'         -- tratada à parte, vira botão de destaque
    local ls = string.lower( s or '' )
local GOOGLE_MAPS_PROPERTY = 'P75' -- tratada com selinho especial na tabela
    for _, pad in ipairs( EXCLUIR_CHIPS ) do
         if ls:find( pad, 1, true ) then return true end
    end
    return ls:find( 'rua ' ) or ls:find( 'avenida' ) or ls:find( 'av. ' ) or false
end


local function getPropertyLabel(pid)
local function esc( s )
    local propEntity = mw.wikibase.getEntity(pid)
     if not s then return '' end
     if not propEntity then return pid end
     return mw.text.encode( tostring( s ), '<>&"' )
     local label = propEntity:getLabel('pt-br')
    if not label then label = propEntity:getLabel('en') end
    if not label then label = pid end
    return label
end
end


local function formatValue(snak)
local function linkExterno( url, texto )
     if not snak.datavalue then return nil end
     if not url or url == '' then return esc( texto or '' ) end
     local datatype = snak.datavalue.type
     return '[' .. url .. ' ' .. ( texto or url ) .. ']'
    local value = snak.datavalue.value
end
    if not value then return nil end


    if datatype == 'wikibase-entityid' then
local function rotulo( ent, lang )
        local targetId = value.id
    if not ent then return nil end
        local targetEntity = mw.wikibase.getEntity(targetId)
    for _, l in ipairs( { 'pt-br', 'pt', 'mul', lang, 'en' } ) do
        if targetEntity then
        local lb = ent:getLabel( l )
            local label = targetEntity:getLabel('pt-br') or targetEntity:getLabel('en')
        if lb then return lb end
            if label then return label end
    end
        end
    return nil
        return targetId
end


     elseif datatype == 'monolingualtext' then
local function descricao( ent, lang )
        return value.text
    if not ent then return '' end
     for _, l in ipairs( { 'pt-br', 'pt', 'mul', lang, 'en' } ) do
        local d = ent:getDescription( l )
        if d then return d end
    end
    return ''
end


     elseif datatype == 'quantity' then
local function snakParaTexto( snak, lang )
         local amount = value.amount
    if not snak or snak.snaktype ~= 'value' then return nil end
         amount = amount:gsub('^%+', '')
    local dv = snak.datavalue
         return amount
    if not dv then return nil end
    if dv.type == 'string' or dv.type == 'url' or dv.type == 'external-id' then
        return dv.value
     elseif dv.type == 'monolingualtext' then
         return dv.value.text
    elseif dv.type == 'quantity' then
         return tostring( dv.value.amount ):gsub( '^%+', '' )
    elseif dv.type == 'time' then
         return ( dv.value.time or '' ):match( '(%d+%-%d+%-%d+)' )
    elseif dv.type == 'wikibase-entityid' then
        local alvo = mw.wikibase.getEntity( dv.value.id )
        return rotulo( alvo, lang ) or dv.value.id
    end
    return nil
end


     elseif datatype == 'string' then
local function valor( ent, pid, lang )
        return value
    if not ent then return nil end
    local sts = ent:getBestStatements( pid )
    if not sts then return nil end
     for _, st in ipairs( sts ) do
        local v = snakParaTexto( st.mainsnak, lang )
        if v and v ~= '' then return v end
    end
    return nil
end


     else
local function valores( ent, pid, lang )
         return tostring(value)
    local out = {}
    if not ent then return out end
    local sts = ent:getBestStatements( pid )
    if not sts then return out end
     for _, st in ipairs( sts ) do
         local v = snakParaTexto( st.mainsnak, lang )
        if v and v ~= '' then out[#out + 1] = v end
     end
     end
    return out
end
end


local function buildOrderedPropertyList(claims)
local function qualificador( ent, pid, qpid, lang )
     local seen = {}
    if not ent then return nil end
     local ordered = {}
     local sts = ent:getBestStatements( pid )
    if not sts or not sts[1] then return nil end
     local qs = sts[1].qualifiers
    if not qs then return nil end
    local lista = qs[qpid]
    if not lista then return nil end
    for _, q in ipairs( lista ) do
        local v = snakParaTexto( q, lang )
        if v then return v end
    end
    return nil
end


     for _, pid in ipairs(PRIORITY_ORDER) do
-- Divide "Q: pergunta?A: resposta" em pares { q, a }
         if claims[pid] and not HIDDEN[pid] and pid ~= IMAGE_PROPERTY and pid ~= CTA_PROPERTY then
local function parseFAQ( texto )
            table.insert(ordered, pid)
     local itens = {}
             seen[pid] = true
    if not texto then return itens end
    if not texto:find( 'Q:', 1, true ) then
        local t = texto:gsub( '^%s+', '' ):gsub( '%s+$', '' )
         if t ~= '' then itens[#itens + 1] = { q = '', a = t } end
        return itens
    end
    local pos = 1
    while true do
        local qs = texto:find( 'Q:', pos, true )
        if not qs then break end
        local qe = texto:find( 'A:', qs + 2, true )
        if not qe then break end
        local pergunta = texto:sub( qs + 2, qe - 1 ):gsub( '^%s+', '' ):gsub( '%s+$', '' )
        local ae = texto:find( 'Q:', qe + 2, true ) or ( #texto + 1 )
        local resposta  = texto:sub( qe + 2, ae - 1 ):gsub( '^%s+', '' ):gsub( '%s+$', '' )
        if pergunta ~= '' then itens[#itens + 1] = { q = pergunta, a = resposta } end
        pos = ae
    end
    return itens
end
 
local function chips( lista )
    local s = ''
    for _, v in ipairs( lista ) do
        if not contemLixo( v ) then
             s = s .. '<span class="chip-especialidade">' .. esc( v ) .. '</span> '
         end
         end
     end
     end
    return s
end
local function estrelas( n )
    n = math.floor( tonumber( n ) or 0 )
    n = math.max( 0, math.min( 5, n ) )
    return string.rep( '★', n ) .. string.rep( '☆', 5 - n )
end
function p.main( frame )
    local args = frame.args
    local id = ( args.ID or args[1] or '' ):match( '^%s*(.-)%s*$' )
    local lang = mw.getContentLanguage():getCode()


     local rest = {}
     local ent = mw.wikibase.getEntity( id ~= '' and id or nil )
     for pid, _ in pairs(claims) do
    if not ent then
        if not seen[pid] and not HIDDEN[pid] and pid ~= IMAGE_PROPERTY and pid ~= CTA_PROPERTY then
        return '<div class="infobox-medico"><div class="infobox-medico-titulo">Entidade não encontrada — informe ID=Q…</div></div>'
            table.insert(rest, pid)
    end
 
    local nome      = valor( ent, CFG.nome, lang ) or rotulo( ent, lang ) or '—'
    local desc      = descricao( ent, lang )
    local foto      = valor( ent, CFG.foto, lang )
    local regs      = valores( ent, CFG.registro, lang )
    local esp        = valor( ent, CFG.especialidade, lang )
    local sub        = valor( ent, CFG.subesp, lang )
    local logradouro = valor( ent, CFG.logradouro, lang )
    local bairro    = valor( ent, CFG.bairro, lang )
    local municipio  = valor( ent, CFG.municipio, lang )
    local estado    = valor( ent, CFG.estado, lang )
    local cep        = valor( ent, CFG.cep, lang )
    local horario    = valor( ent, CFG.horario, lang )
    local convenio  = valor( ent, CFG.convenio, lang )
    local operadora  = valor( ent, CFG.operadora, lang )
    local email      = valor( ent, CFG.email, lang )
    local website    = valor( ent, CFG.website, lang )
    local agenda    = valor( ent, CFG.agendamento, lang )
    local doc        = valor( ent, CFG.doctoralia, lang )
    local estrelasN  = valor( ent, CFG.estrelas, lang )
    local opinioes  = valor( ent, CFG.opinioes, lang )
    local bio        = valor( ent, CFG.bio, lang )
    local cid        = valor( ent, CFG.maps_cid, lang )
    local pubs      = valores( ent, CFG.publicacoes, lang )
    local scholar    = valor( ent, CFG.scholar_user, lang )
    local escopoNeg  = valores( ent, CFG.escopo_neg, lang )
    local fonteTxt  = qualificador( ent, CFG.registro, CFG.fonte, lang ) or qualificador( ent, CFG.bio, CFG.fonte, lang )
    local coletaTxt  = qualificador( ent, CFG.registro, CFG.coleta, lang ) or qualificador( ent, CFG.bio, CFG.coleta, lang )
 
    local servicos = {}
     for _, spid in ipairs( CFG.servicos ) do
        for _, v in ipairs( valores( ent, spid, lang ) ) do
            servicos[#servicos + 1] = v
         end
         end
     end
     end
     table.sort(rest, function(a, b)
 
         return tonumber(a:sub(2)) < tonumber(b:sub(2))
     local faqs = {}
    end)
    for _, t in ipairs( valores( ent, CFG.faq, lang ) ) do
    for _, pid in ipairs(rest) do
         for _, par in ipairs( parseFAQ( t ) ) do
         table.insert(ordered, pid)
            faqs[#faqs + 1] = par
         end
     end
     end


     return ordered
     local out = {}
end
    out[#out + 1] = '<div class="infobox-medico">'


function p.main(frame)
     if foto then
    local id = frame.args.ID
         out[#out + 1] = '<div class="infobox-medico-imagem">'
     if not id then
            .. frame:preprocess( '[[File:' .. foto .. '|300px|center]]' ) .. '</div>'
         return '<strong class="error">Erro: parâmetro ID obrigatório, ex: {{Infobox Médico|ID=Q1}}</strong>'
     end
     end


     local entity = mw.wikibase.getEntity(id)
     out[#out + 1] = '<div class="infobox-medico-titulo">' .. esc( nome ) .. '</div>'
     if not entity then
     if desc and desc ~= '' then
         return '<strong class="error">Item não encontrado: ' .. tostring(id) .. '</strong>'
         out[#out + 1] = '<div class="infobox-medico-descricao">' .. esc( desc ) .. '</div>'
     end
     end


     local label = entity:getLabel('pt-br') or entity:getLabel('en') or id
     local espChips = {}
     local description = entity:getDescription('pt-br') or entity:getDescription('en') or ''
    if esp then espChips[#espChips + 1] = esp end
    if sub then espChips[#espChips + 1] = sub end
     if #espChips > 0 then
        out[#out + 1] = '<div class="infobox-medico-chips">' .. chips( espChips ) .. '</div>'
    end


     local html = mw.html.create('div'):addClass('infobox-medico')
     if estrelasN then
        local nota = '<div class="medico-avaliacao"><span class="medico-estrelas">' .. estrelas( estrelasN )
            .. '</span> <span class="medico-nota">' .. esc( estrelasN ) .. '/5</span>'
        if opinioes then
            if doc then
                nota = nota .. ' · ' .. linkExterno( doc, opinioes .. ' opiniões' )
            else
                nota = nota .. ' · ' .. esc( opinioes ) .. ' opiniões'
            end
        end
        out[#out + 1] = nota .. '</div>'
    end


     -- Imagem (P1). Se sua lógica atual pra imagem for diferente, mantenha a que já
     out[#out + 1] = '<table class="infobox-medico-tabela">'
     -- funciona e só copie o resto (prioridade/ocultação/pt-br/CTA).
     local function linha( r, v )
    if entity.claims and entity.claims[IMAGE_PROPERTY] then
        if v and v ~= '' then
        local imgClaim = entity.claims[IMAGE_PROPERTY][1]
             return '<tr><th>' .. esc( r ) .. '</th><td>' .. v .. '</td></tr>'
        local imgValue = imgClaim.mainsnak.datavalue and imgClaim.mainsnak.datavalue.value
        if imgValue then
             local fileName = imgValue.fileName or imgValue
            html:tag('div'):addClass('infobox-medico-imagem')
                :wikitext('[[File:' .. fileName .. '|300px]]')
         end
         end
        return ''
    end
    if #regs > 0 then
        local regHtml = {}
        for _, r in ipairs( regs ) do regHtml[#regHtml + 1] = esc( r ) end
        out[#out + 1] = linha( 'Registro', table.concat( regHtml, '<br>' ) )
    end
    local partesEnd = {}
    for _, v in ipairs( { logradouro, bairro, municipio, estado, cep } ) do
        if v and v ~= '' then partesEnd[#partesEnd + 1] = v end
    end
    if #partesEnd > 0 then
        out[#out + 1] = linha( 'Endereço', esc( table.concat( partesEnd, ', ' ) ) )
    end
    if horario  then out[#out + 1] = linha( 'Horário', esc( horario ) ) end
    if convenio  then out[#out + 1] = linha( 'Convênio', esc( convenio ) ) end
    if operadora then out[#out + 1] = linha( 'Operadora', esc( operadora ) ) end
    if email    then out[#out + 1] = linha( 'E-mail', linkExterno( 'mailto:' .. email, email ) ) end
    if website then
        local siteHref = website:match( '^https?://' ) and website or ( 'https://' .. website )
        out[#out + 1] = linha( 'Site', linkExterno( siteHref, website ) )
    end
    if doc then
        out[#out + 1] = linha( 'Doctoralia', linkExterno( doc, 'Ver perfil' ) )
    end
    out[#out + 1] = '</table>'
    if bio and bio ~= '' then
        out[#out + 1] = '<div class="infobox-medico-bio">' .. esc( bio ) .. '</div>'
    end
    if #servicos > 0 then
        out[#out + 1] = '<div class="infobox-medico-secao">Procedimentos e serviços</div>'
        out[#out + 1] = '<div class="infobox-medico-chips">' .. chips( servicos ) .. '</div>'
    end
    if agenda then
        out[#out + 1] = '<div class="cta-agendar">' .. linkExterno( agenda, 'Agendar consulta' ) .. '</div>'
     end
     end


     html:tag('div'):addClass('infobox-medico-titulo'):wikitext(label)
     -- Mapa v3.3: chama a extensão diretamente (sem checagem impossível de <iframe>
    if description ~= '' then
    -- dentro do Lua) usando a URL já validada via API (CID + maps.google.com).
         html:tag('div'):addClass('infobox-medico-descricao'):wikitext(description)
    -- O link "Ver no Google Meu Negócio" sempre aparece, como reforço.
    if cid and cid ~= '' or logradouro then
        local endMapa = table.concat( { logradouro, bairro, municipio, estado }, ', ' )
        local urlMapa
        if cid and cid ~= '' then
            urlMapa = 'https://maps.google.com/maps?cid=' .. cid .. '&output=embed&hl=pt-BR'
        elseif endMapa ~= '' then
            urlMapa = 'https://maps.google.com/maps?q=' .. mw.uri.encode( endMapa, 'QUERY' )
                .. '&z=16&output=embed&hl=pt-BR'
        end
 
        if urlMapa then
            local ok, html = pcall( frame.extensionTag, frame, 'perfilexterno', '', {
                url = urlMapa, largura = '100%', altura = '300'
            } )
            if ok and html then
                out[#out + 1] = '<div class="medico-mapa">' .. html .. '</div>'
            end
        end
 
        local urlNegocio
        if cid and cid ~= '' then
            urlNegocio = 'https://www.google.com/maps?cid=' .. cid
         elseif endMapa ~= '' then
            urlNegocio = 'https://maps.google.com/maps?q=' .. mw.uri.encode( endMapa, 'QUERY' )
        end
        if urlNegocio then
            out[#out + 1] = '<div class="medico-mapa-link">'
                .. linkExterno( urlNegocio, 'Ver no Google Meu Negócio' ) .. '</div>'
        end
     end
     end


     -- Botão de agendamento (P32), em destaque, sem emoji
     if #pubs > 0 then
    if entity.claims and entity.claims[CTA_PROPERTY] then
        out[#out + 1] = '<div class="infobox-medico-secao">Publicações científicas</div>'
         local ctaClaim = entity.claims[CTA_PROPERTY][1]
        out[#out + 1] = '<ul class="infobox-medico-pubs">'
        local ctaUrl = ctaClaim.mainsnak.datavalue and ctaClaim.mainsnak.datavalue.value
         local n = math.min( 5, #pubs )
         if ctaUrl then
        for i = 1, n do
             html:tag('div'):addClass('cta-agendar')
            local u = pubs[i]
                 :wikitext('[' .. ctaUrl .. ' Agendar consulta pelo WhatsApp]')
            out[#out + 1] = '<li>' .. linkExterno( u, u:gsub( '^https?://', '' ):gsub( '^www%.', '' ) ) .. '</li>'
        end
        out[#out + 1] = '</ul>'
         if #pubs > n then
             local resto = #pubs - n
            local alvo = scholar and ( 'https://scholar.google.com/citations?user=' .. scholar .. '&hl=pt-BR' ) or ''
            if alvo ~= '' then
                 out[#out + 1] = '<div class="medico-mais-pubs">'
                    .. linkExterno( alvo, '+ ' .. resto .. ' publicações no Google Scholar' ) .. '</div>'
            else
                out[#out + 1] = '<div class="medico-mais-pubs">+ ' .. resto .. ' publicações científicas</div>'
            end
         end
         end
     end
     end


     local tableTag = html:tag('table'):addClass('infobox-medico-tabela')
     if #escopoNeg > 0 then
        out[#out + 1] = '<div class="medico-desamb">'
        out[#out + 1] = '<div class="medico-desamb-titulo">Desambiguação — o que este perfil <b>não</b> é</div>'
        out[#out + 1] = '<ul class="medico-desamb-lista">'
        for _, v in ipairs( escopoNeg ) do
            out[#out + 1] = '<li>' .. esc( v ) .. '</li>'
        end
        out[#out + 1] = '</ul></div>'
    end


     if entity.claims then
     if #faqs > 0 then
         local orderedPids = buildOrderedPropertyList(entity.claims)
         out[#out + 1] = '<div class="infobox-medico-secao">Perguntas Frequentes</div>'
         for _, pid in ipairs(orderedPids) do
         for _, par in ipairs( faqs ) do
             local propLabel = getPropertyLabel(pid)
             out[#out + 1] = '<div class="medico-faq">'
            local values = {}
            if par.q ~= '' then
            for _, claim in ipairs(entity.claims[pid]) do
                out[#out + 1] = '<div class="medico-faq-q">' .. esc( par.q ) .. '</div>'
                local formatted = formatValue(claim.mainsnak)
                if formatted then
                    table.insert(values, formatted)
                end
            end
            if #values > 0 then
                local valueHtml
                if pid == GOOGLE_MAPS_PROPERTY then
                    local cid = values[1]
                    local mapsUrl = 'https://maps.google.com/?cid=' .. cid
                    valueHtml = '<span class="google-maps-badge">[' .. mapsUrl ..
                        ' <span class="g-blue">G</span><span class="g-red">o</span>' ..
                        '<span class="g-yellow">o</span><span class="g-blue">g</span>' ..
                        '<span class="g-green">l</span><span class="g-red">e</span>' ..
                        '<span class="g-text">Ver no Mapa</span>]</span>'
                    propLabel = 'Localização'
                elseif #values > 1 then
                    local ul = mw.html.create('ul')
                    for _, v in ipairs(values) do
                        ul:tag('li'):wikitext(v)
                    end
                    valueHtml = tostring(ul)
                else
                    valueHtml = values[1]
                end
                local row = tableTag:tag('tr')
                row:tag('th'):wikitext(propLabel)
                row:tag('td'):wikitext(valueHtml)
             end
             end
            out[#out + 1] = '<div class="medico-faq-a">' .. esc( par.a ) .. '</div>'
            out[#out + 1] = '</div>'
         end
         end
     end
     end


     return tostring(html)
    if fonteTxt or coletaTxt then
        out[#out + 1] = '<div class="infobox-medico-rodape">Dados curados · fonte: ' .. esc( fonteTxt or '—' )
        if coletaTxt then out[#out + 1] = ' · verificado em ' .. esc( coletaTxt ) end
        out[#out + 1] = '</div>'
    end
 
    out[#out + 1] = '</div>'
     return table.concat( out )
end
end


return p
return p

Latest revision as of 21:51, 3 August 2026

Documentation for this module may be created at Module:MedicoInfobox/doc

-- Module:MedicoInfobox v3.3
-- FAQ (P56) e Desambiguação (P74) em divs (tags permitidas pelo Sanitizer)
-- Mapa: chamada direta via extensionTag (sem checagem no Lua — extensionTag só
-- devolve um strip marker UNIQ...QINU, nunca o HTML final, então validar dentro
-- do Lua é impossível; confiamos direto na URL já testada e aprovada)
local p = {}

local CFG = {
    foto          = 'P1',
    nome          = 'P2',
    registro      = 'P4',
    especialidade = 'P10',
    subesp        = 'P11',
    servicos      = { 'P13', 'P14', 'P18', 'P19', 'P23' },
    logradouro    = 'P28',
    bairro        = 'P29',
    email         = 'P30',
    website       = 'P31',
    agendamento   = 'P32',
    lat           = 'P33',
    lon           = 'P34',
    convenio      = 'P35',
    estado        = 'P36',
    operadora     = 'P37',
    opinioes      = 'P38',
    estrelas      = 'P39',
    duvidas       = 'P40',
    faq           = 'P56',
    doctoralia    = 'P57',
    scholar_user  = 'P62',
    municipio     = 'P71',
    cep           = 'P72',
    bio           = 'P73',
    escopo_neg    = 'P74',
    maps_cid      = 'P75',
    horario       = 'P76',
    publicacoes   = 'P77',
    fonte         = 'P79',
    coleta        = 'P80',
}

local EXCLUIR_CHIPS = { 'unimed', 'vivest', 'seguros' }

local function contemLixo( s )
    local ls = string.lower( s or '' )
    for _, pad in ipairs( EXCLUIR_CHIPS ) do
        if ls:find( pad, 1, true ) then return true end
    end
    return ls:find( 'rua ' ) or ls:find( 'avenida' ) or ls:find( 'av. ' ) or false
end

local function esc( s )
    if not s then return '' end
    return mw.text.encode( tostring( s ), '<>&"' )
end

local function linkExterno( url, texto )
    if not url or url == '' then return esc( texto or '' ) end
    return '[' .. url .. ' ' .. ( texto or url ) .. ']'
end

local function rotulo( ent, lang )
    if not ent then return nil end
    for _, l in ipairs( { 'pt-br', 'pt', 'mul', lang, 'en' } ) do
        local lb = ent:getLabel( l )
        if lb then return lb end
    end
    return nil
end

local function descricao( ent, lang )
    if not ent then return '' end
    for _, l in ipairs( { 'pt-br', 'pt', 'mul', lang, 'en' } ) do
        local d = ent:getDescription( l )
        if d then return d end
    end
    return ''
end

local function snakParaTexto( snak, lang )
    if not snak or snak.snaktype ~= 'value' then return nil end
    local dv = snak.datavalue
    if not dv then return nil end
    if dv.type == 'string' or dv.type == 'url' or dv.type == 'external-id' then
        return dv.value
    elseif dv.type == 'monolingualtext' then
        return dv.value.text
    elseif dv.type == 'quantity' then
        return tostring( dv.value.amount ):gsub( '^%+', '' )
    elseif dv.type == 'time' then
        return ( dv.value.time or '' ):match( '(%d+%-%d+%-%d+)' )
    elseif dv.type == 'wikibase-entityid' then
        local alvo = mw.wikibase.getEntity( dv.value.id )
        return rotulo( alvo, lang ) or dv.value.id
    end
    return nil
end

local function valor( ent, pid, lang )
    if not ent then return nil end
    local sts = ent:getBestStatements( pid )
    if not sts then return nil end
    for _, st in ipairs( sts ) do
        local v = snakParaTexto( st.mainsnak, lang )
        if v and v ~= '' then return v end
    end
    return nil
end

local function valores( ent, pid, lang )
    local out = {}
    if not ent then return out end
    local sts = ent:getBestStatements( pid )
    if not sts then return out end
    for _, st in ipairs( sts ) do
        local v = snakParaTexto( st.mainsnak, lang )
        if v and v ~= '' then out[#out + 1] = v end
    end
    return out
end

local function qualificador( ent, pid, qpid, lang )
    if not ent then return nil end
    local sts = ent:getBestStatements( pid )
    if not sts or not sts[1] then return nil end
    local qs = sts[1].qualifiers
    if not qs then return nil end
    local lista = qs[qpid]
    if not lista then return nil end
    for _, q in ipairs( lista ) do
        local v = snakParaTexto( q, lang )
        if v then return v end
    end
    return nil
end

-- Divide "Q: pergunta?A: resposta" em pares { q, a }
local function parseFAQ( texto )
    local itens = {}
    if not texto then return itens end
    if not texto:find( 'Q:', 1, true ) then
        local t = texto:gsub( '^%s+', '' ):gsub( '%s+$', '' )
        if t ~= '' then itens[#itens + 1] = { q = '', a = t } end
        return itens
    end
    local pos = 1
    while true do
        local qs = texto:find( 'Q:', pos, true )
        if not qs then break end
        local qe = texto:find( 'A:', qs + 2, true )
        if not qe then break end
        local pergunta = texto:sub( qs + 2, qe - 1 ):gsub( '^%s+', '' ):gsub( '%s+$', '' )
        local ae = texto:find( 'Q:', qe + 2, true ) or ( #texto + 1 )
        local resposta  = texto:sub( qe + 2, ae - 1 ):gsub( '^%s+', '' ):gsub( '%s+$', '' )
        if pergunta ~= '' then itens[#itens + 1] = { q = pergunta, a = resposta } end
        pos = ae
    end
    return itens
end

local function chips( lista )
    local s = ''
    for _, v in ipairs( lista ) do
        if not contemLixo( v ) then
            s = s .. '<span class="chip-especialidade">' .. esc( v ) .. '</span> '
        end
    end
    return s
end

local function estrelas( n )
    n = math.floor( tonumber( n ) or 0 )
    n = math.max( 0, math.min( 5, n ) )
    return string.rep( '★', n ) .. string.rep( '☆', 5 - n )
end

function p.main( frame )
    local args = frame.args
    local id = ( args.ID or args[1] or '' ):match( '^%s*(.-)%s*$' )
    local lang = mw.getContentLanguage():getCode()

    local ent = mw.wikibase.getEntity( id ~= '' and id or nil )
    if not ent then
        return '<div class="infobox-medico"><div class="infobox-medico-titulo">Entidade não encontrada — informe ID=Q…</div></div>'
    end

    local nome       = valor( ent, CFG.nome, lang ) or rotulo( ent, lang ) or '—'
    local desc       = descricao( ent, lang )
    local foto       = valor( ent, CFG.foto, lang )
    local regs       = valores( ent, CFG.registro, lang )
    local esp        = valor( ent, CFG.especialidade, lang )
    local sub        = valor( ent, CFG.subesp, lang )
    local logradouro = valor( ent, CFG.logradouro, lang )
    local bairro     = valor( ent, CFG.bairro, lang )
    local municipio  = valor( ent, CFG.municipio, lang )
    local estado     = valor( ent, CFG.estado, lang )
    local cep        = valor( ent, CFG.cep, lang )
    local horario    = valor( ent, CFG.horario, lang )
    local convenio   = valor( ent, CFG.convenio, lang )
    local operadora  = valor( ent, CFG.operadora, lang )
    local email      = valor( ent, CFG.email, lang )
    local website    = valor( ent, CFG.website, lang )
    local agenda     = valor( ent, CFG.agendamento, lang )
    local doc        = valor( ent, CFG.doctoralia, lang )
    local estrelasN  = valor( ent, CFG.estrelas, lang )
    local opinioes   = valor( ent, CFG.opinioes, lang )
    local bio        = valor( ent, CFG.bio, lang )
    local cid        = valor( ent, CFG.maps_cid, lang )
    local pubs       = valores( ent, CFG.publicacoes, lang )
    local scholar    = valor( ent, CFG.scholar_user, lang )
    local escopoNeg  = valores( ent, CFG.escopo_neg, lang )
    local fonteTxt   = qualificador( ent, CFG.registro, CFG.fonte, lang ) or qualificador( ent, CFG.bio, CFG.fonte, lang )
    local coletaTxt  = qualificador( ent, CFG.registro, CFG.coleta, lang ) or qualificador( ent, CFG.bio, CFG.coleta, lang )

    local servicos = {}
    for _, spid in ipairs( CFG.servicos ) do
        for _, v in ipairs( valores( ent, spid, lang ) ) do
            servicos[#servicos + 1] = v
        end
    end

    local faqs = {}
    for _, t in ipairs( valores( ent, CFG.faq, lang ) ) do
        for _, par in ipairs( parseFAQ( t ) ) do
            faqs[#faqs + 1] = par
        end
    end

    local out = {}
    out[#out + 1] = '<div class="infobox-medico">'

    if foto then
        out[#out + 1] = '<div class="infobox-medico-imagem">'
            .. frame:preprocess( '[[File:' .. foto .. '|300px|center]]' ) .. '</div>'
    end

    out[#out + 1] = '<div class="infobox-medico-titulo">' .. esc( nome ) .. '</div>'
    if desc and desc ~= '' then
        out[#out + 1] = '<div class="infobox-medico-descricao">' .. esc( desc ) .. '</div>'
    end

    local espChips = {}
    if esp then espChips[#espChips + 1] = esp end
    if sub then espChips[#espChips + 1] = sub end
    if #espChips > 0 then
        out[#out + 1] = '<div class="infobox-medico-chips">' .. chips( espChips ) .. '</div>'
    end

    if estrelasN then
        local nota = '<div class="medico-avaliacao"><span class="medico-estrelas">' .. estrelas( estrelasN )
            .. '</span> <span class="medico-nota">' .. esc( estrelasN ) .. '/5</span>'
        if opinioes then
            if doc then
                nota = nota .. ' · ' .. linkExterno( doc, opinioes .. ' opiniões' )
            else
                nota = nota .. ' · ' .. esc( opinioes ) .. ' opiniões'
            end
        end
        out[#out + 1] = nota .. '</div>'
    end

    out[#out + 1] = '<table class="infobox-medico-tabela">'
    local function linha( r, v )
        if v and v ~= '' then
            return '<tr><th>' .. esc( r ) .. '</th><td>' .. v .. '</td></tr>'
        end
        return ''
    end

    if #regs > 0 then
        local regHtml = {}
        for _, r in ipairs( regs ) do regHtml[#regHtml + 1] = esc( r ) end
        out[#out + 1] = linha( 'Registro', table.concat( regHtml, '<br>' ) )
    end

    local partesEnd = {}
    for _, v in ipairs( { logradouro, bairro, municipio, estado, cep } ) do
        if v and v ~= '' then partesEnd[#partesEnd + 1] = v end
    end
    if #partesEnd > 0 then
        out[#out + 1] = linha( 'Endereço', esc( table.concat( partesEnd, ', ' ) ) )
    end

    if horario   then out[#out + 1] = linha( 'Horário', esc( horario ) ) end
    if convenio  then out[#out + 1] = linha( 'Convênio', esc( convenio ) ) end
    if operadora then out[#out + 1] = linha( 'Operadora', esc( operadora ) ) end
    if email     then out[#out + 1] = linha( 'E-mail', linkExterno( 'mailto:' .. email, email ) ) end
    if website then
        local siteHref = website:match( '^https?://' ) and website or ( 'https://' .. website )
        out[#out + 1] = linha( 'Site', linkExterno( siteHref, website ) )
    end
    if doc then
        out[#out + 1] = linha( 'Doctoralia', linkExterno( doc, 'Ver perfil' ) )
    end
    out[#out + 1] = '</table>'

    if bio and bio ~= '' then
        out[#out + 1] = '<div class="infobox-medico-bio">' .. esc( bio ) .. '</div>'
    end

    if #servicos > 0 then
        out[#out + 1] = '<div class="infobox-medico-secao">Procedimentos e serviços</div>'
        out[#out + 1] = '<div class="infobox-medico-chips">' .. chips( servicos ) .. '</div>'
    end

    if agenda then
        out[#out + 1] = '<div class="cta-agendar">' .. linkExterno( agenda, 'Agendar consulta' ) .. '</div>'
    end

    -- Mapa v3.3: chama a extensão diretamente (sem checagem impossível de <iframe>
    -- dentro do Lua) usando a URL já validada via API (CID + maps.google.com).
    -- O link "Ver no Google Meu Negócio" sempre aparece, como reforço.
    if cid and cid ~= '' or logradouro then
        local endMapa = table.concat( { logradouro, bairro, municipio, estado }, ', ' )
        local urlMapa
        if cid and cid ~= '' then
            urlMapa = 'https://maps.google.com/maps?cid=' .. cid .. '&output=embed&hl=pt-BR'
        elseif endMapa ~= '' then
            urlMapa = 'https://maps.google.com/maps?q=' .. mw.uri.encode( endMapa, 'QUERY' )
                .. '&z=16&output=embed&hl=pt-BR'
        end

        if urlMapa then
            local ok, html = pcall( frame.extensionTag, frame, 'perfilexterno', '', {
                url = urlMapa, largura = '100%', altura = '300'
            } )
            if ok and html then
                out[#out + 1] = '<div class="medico-mapa">' .. html .. '</div>'
            end
        end

        local urlNegocio
        if cid and cid ~= '' then
            urlNegocio = 'https://www.google.com/maps?cid=' .. cid
        elseif endMapa ~= '' then
            urlNegocio = 'https://maps.google.com/maps?q=' .. mw.uri.encode( endMapa, 'QUERY' )
        end
        if urlNegocio then
            out[#out + 1] = '<div class="medico-mapa-link">'
                .. linkExterno( urlNegocio, 'Ver no Google Meu Negócio' ) .. '</div>'
        end
    end

    if #pubs > 0 then
        out[#out + 1] = '<div class="infobox-medico-secao">Publicações científicas</div>'
        out[#out + 1] = '<ul class="infobox-medico-pubs">'
        local n = math.min( 5, #pubs )
        for i = 1, n do
            local u = pubs[i]
            out[#out + 1] = '<li>' .. linkExterno( u, u:gsub( '^https?://', '' ):gsub( '^www%.', '' ) ) .. '</li>'
        end
        out[#out + 1] = '</ul>'
        if #pubs > n then
            local resto = #pubs - n
            local alvo = scholar and ( 'https://scholar.google.com/citations?user=' .. scholar .. '&hl=pt-BR' ) or ''
            if alvo ~= '' then
                out[#out + 1] = '<div class="medico-mais-pubs">'
                    .. linkExterno( alvo, '+ ' .. resto .. ' publicações no Google Scholar' ) .. '</div>'
            else
                out[#out + 1] = '<div class="medico-mais-pubs">+ ' .. resto .. ' publicações científicas</div>'
            end
        end
    end

    if #escopoNeg > 0 then
        out[#out + 1] = '<div class="medico-desamb">'
        out[#out + 1] = '<div class="medico-desamb-titulo">Desambiguação — o que este perfil <b>não</b> é</div>'
        out[#out + 1] = '<ul class="medico-desamb-lista">'
        for _, v in ipairs( escopoNeg ) do
            out[#out + 1] = '<li>' .. esc( v ) .. '</li>'
        end
        out[#out + 1] = '</ul></div>'
    end

    if #faqs > 0 then
        out[#out + 1] = '<div class="infobox-medico-secao">Perguntas Frequentes</div>'
        for _, par in ipairs( faqs ) do
            out[#out + 1] = '<div class="medico-faq">'
            if par.q ~= '' then
                out[#out + 1] = '<div class="medico-faq-q">' .. esc( par.q ) .. '</div>'
            end
            out[#out + 1] = '<div class="medico-faq-a">' .. esc( par.a ) .. '</div>'
            out[#out + 1] = '</div>'
        end
    end

    if fonteTxt or coletaTxt then
        out[#out + 1] = '<div class="infobox-medico-rodape">Dados curados · fonte: ' .. esc( fonteTxt or '—' )
        if coletaTxt then out[#out + 1] = ' · verificado em ' .. esc( coletaTxt ) end
        out[#out + 1] = '</div>'
    end

    out[#out + 1] = '</div>'
    return table.concat( out )
end

return p