"""Rebusca os 2 canais na API OFICIAL do YouTube e regenera o dashboard_youtube.html.

Uso:
    python atualizar_dashboard.py

Requisitos (uma vez):
    - token_psoriless.json  -> rode `python auth_psoriless.py` e faça login com a conta do canal Psoríase
    - token_youtube.json    -> já existe (canal Hemorroidas)

O script detecta sozinho o último dia com dados e ajusta as janelas (30d / 7d / dia).
Obs.: as datas internas usam MM-DD; se a janela de 30 dias cruzar a virada de ano, me avise para ajustar.
"""
import os, sys, json, re
from datetime import date, timedelta
try: sys.stdout.reconfigure(encoding="utf-8")
except Exception: pass
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build

BASE = os.path.dirname(os.path.abspath(__file__))
HTML = os.path.join(BASE, "dashboard_youtube.html")
SCOPES = ["https://www.googleapis.com/auth/yt-analytics.readonly",
          "https://www.googleapis.com/auth/youtube.readonly"]
# (token, chave, classe-cor, cor, indices no array daily [short,long])
CANAIS = [
    ("token_psoriless.json",   "psoriase",    "p", "#7c3aed", {"short":3, "long":4}),
    ("token_youtube.json",     "hemorroidas", "h", "#db2777", {"short":1, "long":2}),
    ("token_foliculite.json",  "foliculite",  "f", "#0d9488", {"short":5, "long":6}),
    ("token_hiperidrose.json", "hiperidrose", "i", "#ea580c", {"short":7, "long":8}),
]

def addd(iso, n): return (date.fromisoformat(iso) + timedelta(days=n)).isoformat()

def creds_for(tok):
    path = os.path.join(BASE, tok)
    if not os.path.exists(path):
        sys.exit(f"ERRO: falta {tok}. Gere o token primeiro (auth_psoriless.py para o Psoríase).")
    c = Credentials.from_authorized_user_file(path, SCOPES)
    if not c.valid and c.expired and c.refresh_token:
        c.refresh(Request()); open(path, "w").write(c.to_json())
    return c

def detect_ref(ya):
    today = date.today().isoformat()
    rows = ya.reports().query(ids="channel==MINE", startDate=addd(today,-10), endDate=today,
                              dimensions="day", metrics="views").execute().get("rows", [])
    days = [r[0] for r in rows if r[1]]
    return max(days) if days else addd(today, -3)

def fetch(tok, ref):
    creds = creds_for(tok)
    yt = build("youtube", "v3", credentials=creds)
    ya = build("youtubeAnalytics", "v2", credentials=creds)
    def q(**kw): return ya.reports().query(ids="channel==MINE", **kw).execute()
    WIN = {"30d":(addd(ref,-29),ref), "30dc":(addd(ref,-59),addd(ref,-30)),
           "7d":(addd(ref,-6),ref),   "7dc":(addd(ref,-13),addd(ref,-7)),
           "dia":(ref,ref),           "diac":(addd(ref,-1),addd(ref,-1))}
    def agg(ini,fim):
        r=q(startDate=ini,endDate=fim,metrics="views,averageViewPercentage,subscribersGained,subscribersLost").get("rows",[[0,0,0,0]])[0]
        tv=int(r[0]); vs=0
        for row in q(startDate=ini,endDate=fim,metrics="views",dimensions="creatorContentType").get("rows",[]):
            if row[0]=="shorts": vs+=int(row[1])
        return {"views":tv,"views_short":vs,"views_long":tv-vs,"retention":round(r[1] or 0,1),"net_subs":int(r[2])-int(r[3])}
    def traf(ini,fim):
        r=q(startDate=ini,endDate=fim,metrics="views",dimensions="insightTrafficSourceType",sort="-views",maxResults=20)
        return [[x[0],int(x[1])] for x in r.get("rows",[])]
    def topv(ini,fim):
        rows=q(startDate=ini,endDate=fim,metrics="views,averageViewPercentage",dimensions="video",sort="-views",maxResults=8).get("rows",[])
        ids=[x[0] for x in rows]; titles={}; sset=set()
        if ids:
            vd=yt.videos().list(part="snippet",id=",".join(ids)).execute()
            titles={i["id"]:i["snippet"]["title"] for i in vd["items"]}
            # tipo (short/longo) pela fonte oficial: creatorContentType==shorts
            try:
                for x in q(startDate=ini,endDate=fim,metrics="views",dimensions="video",filters="creatorContentType==shorts",sort="-views",maxResults=50).get("rows",[]):
                    sset.add(x[0])
            except Exception: pass
        # [titulo, views, retenção, videoId, tipo("s"=short/"v"=longo)]
        return [[titles.get(x[0],x[0]),int(x[1]),round(x[2] or 0,1),x[0],("s" if x[0] in sset else "v")] for x in rows]
    def buscas(ini,fim):
        try:
            r=q(startDate=ini,endDate=fim,metrics="views",dimensions="insightTrafficSourceDetail",
                filters="insightTrafficSourceType==YT_SEARCH",sort="-views",maxResults=10)
            return [[x[0],int(x[1])] for x in r.get("rows",[])]
        except Exception: return []
    info=yt.channels().list(part="snippet,statistics,contentDetails",mine=True).execute()["items"][0]
    uploads=info["contentDetails"]["relatedPlaylists"]["uploads"]; st=info["statistics"]
    per={k:{"a":agg(*WIN[k]),"b":agg(*WIN[k+"c"]),"top":topv(*WIN[k]),"traf":traf(*WIN[k])} for k in ["30d","7d","dia"]}
    bus={k:buscas(*WIN[k]) for k in ["30d","7d","dia"]}
    # diária por tipo (shorts/longo) na janela 30d
    dt={}
    for row in q(startDate=WIN["30d"][0],endDate=ref,metrics="views",dimensions="day,creatorContentType").get("rows",[]):
        d=dt.setdefault(row[0][5:],[0,0]); d[0 if row[1]=="shorts" else 1]+=int(row[2])
    daily_type=[[k,dt[k][0],dt[k][1]] for k in sorted(dt)]
    # diária agregada (retenção + inscritos)
    dg={}
    for row in q(startDate=WIN["30d"][0],endDate=ref,metrics="views,averageViewPercentage,subscribersGained,subscribersLost",dimensions="day").get("rows",[]):
        dg[row[0][5:]]=[round(row[2] or 0,2),int(row[3])-int(row[4])]
    # posts (data + tipo) na janela 30dc..ref
    pub_from, pub_to = WIN["30dc"][0], addd(ref,1)
    shorts_ids=set()
    try:
        for r in q(startDate=pub_from,endDate=pub_to,metrics="views",dimensions="video",filters="creatorContentType==shorts",sort="-views",maxResults=200).get("rows",[]):
            shorts_ids.add(r[0])
    except Exception as e: print("SHORTS_ERR",e,file=sys.stderr)
    seen=set(); pub=[]; pt=None
    while True:
        pl=yt.playlistItems().list(playlistId=uploads,part="contentDetails",maxResults=50,pageToken=pt).execute()
        for it in pl["items"]:
            vid=it["contentDetails"]["videoId"]; pa=it["contentDetails"].get("videoPublishedAt")
            if not pa or vid in seen: continue
            seen.add(vid); d=pa[:10]
            if pub_from<=d<=pub_to: pub.append([d[5:],"s" if vid in shorts_ids else "v"])
        pt=pl.get("nextPageToken")
        if not pt: break
    pub.sort()
    # agendados + privados/rascunho (não publicados), classificados por tipo (short/longo)
    def _dur(iso):
        m=re.match(r'PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?', iso or 'PT0S')
        h,mi,ss=(int(x) if x else 0 for x in m.groups()); return h*3600+mi*60+ss
    # obs: a playlist de uploads só lista vídeos PÚBLICOS; para enxergar
    # privados/agendados/rascunho do dono é preciso usar search.list(forMine=True)
    pend_ids=[]; ptp=None
    while True:
        sr=yt.search().list(part="id",forMine=True,type="video",maxResults=50,pageToken=ptp).execute()
        pend_ids+=[it["id"]["videoId"] for it in sr["items"]]
        ptp=sr.get("nextPageToken")
        if not ptp: break
    # classificação short/longo pela proporção da tela do player (embedHtml):
    # vertical (largura<altura) => Short; horizontal => vídeo longo.
    # (creatorContentType do Analytics não existe p/ vídeos sem views)
    pend_l=pend_s=0
    for i in range(0,len(pend_ids),50):
        for v in yt.videos().list(part="status,player",id=",".join(pend_ids[i:i+50]),maxHeight=9999).execute()["items"]:
            if v["status"].get("privacyStatus")=="public": continue
            emb=v.get("player",{}).get("embedHtml","")
            mw=re.search(r'width="(\d+)"',emb); mh=re.search(r'height="(\d+)"',emb)
            w=int(mw.group(1)) if mw else 0; h=int(mh.group(1)) if mh else 0
            if w and h and w<h: pend_s+=1
            else: pend_l+=1
    return {"nome":info["snippet"]["title"],"id":info["id"],
            "inscritos":int(st.get("subscriberCount",0)),"views_total":int(st.get("viewCount",0)),
            "videos":int(st.get("videoCount",0)),"per":per,"buscas":bus,"pub":pub,
            "pend_long":pend_l,"pend_short":pend_s,
            "daily_type":daily_type,"dagg":dg,"_win":WIN}

def pct(a,b): return 0 if not b else round((a-b)/b*100)
def build_plano(d):
    WIN=d["_win"]; a=d["per"]["30d"]["a"]; b=d["per"]["30d"]["b"]
    v30,v30c=a["views"],b["views"]; ch=pct(v30,v30c); ret=a["retention"]; subs=a["net_subs"]; subsc=b["net_subs"]
    p7=d["per"]["7d"]; v7,v7c=p7["a"]["views"],p7["b"]["views"]; ch7=pct(v7,v7c)
    tr=dict(d["per"]["30d"]["traf"]); sh=tr.get("SHORTS",0); se=tr.get("YT_SEARCH",0)
    sh_pct=round(sh/v30*100) if v30 else 0; se_pct=round(se/v30*100) if v30 else 0
    lo,hi=WIN["30d"][0][5:],WIN["30d"][1][5:]
    pv=sum(1 for x in d["pub"] if lo<=x[0]<=hi and x[1]=="v"); ps=sum(1 for x in d["pub"] if lo<=x[0]<=hi and x[1]=="s")
    top=d["per"]["30d"]["top"]; top1=(top[0][0][:44] if top else "—"); bus=d["buscas"].get("30d") or []
    sg=lambda x:("+"+str(x)) if x>=0 else str(x); cg=lambda x:("+"+str(x)+"%") if x>=0 else (str(x)+"%")
    dormant=(pv+ps)==0
    if dormant:
        return {"fortes":[
            f"Retenção alta: {ret:.0f}% no período — o conteúdo segura bem quem assiste.",
            f"Busca orgânica é o motor: ~{se_pct}% das views ({se} de {v30}) vêm da Pesquisa do YouTube — tráfego que independe de postar.",
            f"Acervo ainda performa: '{top1}' e outros vídeos captam busca sem nenhuma postagem nova."],
          "melhorias":[
            f"Canal parado: 0 posts no período e views em {cg(ch)} vs o anterior ({v30} vs {v30c}) — sem novos vídeos a distribuição encolhe.",
            f"Descoberta por Shorts/recomendação baixa (Shorts só {sh} views no mês).",
            "Ganho de inscritos e engajamento mínimos, reflexo da inatividade."],
          "frequencia":"Retomar já: no mínimo 3–4 Shorts/semana + 1 vídeo longo/semana, mirando os termos de busca que já trazem tráfego.",
          "conteudo":"Responder às buscas que já geram views"+(f" (ex.: '{bus[0][0]}')" if bus else "")+". Cada termo do Top buscas vira um Short + um vídeo.",
          "estrategia":"Nicho search-driven (SEO): publicar com o título igual à busca literal do painel — o canal já ranqueia nesses termos, então retomar deve recuperar rápido o tráfego perdido.",
          "atencao":f"Queda de {abs(ch)}% no mês por inatividade, apesar de um ativo de SEO valioso. Cada semana parado perde ranqueamento — é urgente retomar."}
    return {"fortes":[
            f"Cadência de publicação: {pv} vídeos + {ps} Shorts no período — regularidade que sustenta a distribuição.",
            (f"Shorts dominam: ~{sh_pct}% das views ({sh} de {v30}) vêm de Shorts." if sh_pct>=se_pct else f"Busca forte: ~{se_pct}% das views ({se} de {v30}) vêm da Pesquisa do YouTube."),
            (f"Recuperação recente: últimos 7 dias com {v7} views vs {v7c} na semana anterior ({cg(ch7)})." if ch7>=0 else f"Retenção em {ret:.0f}% mantém o canal ativo apesar da semana mais fraca.")],
          "melhorias":[
            (f"Queda no mês: {v30} views vs {v30c} no período anterior ({cg(ch)})." if ch<0 else f"Crescimento no mês: {cg(ch)} ({v30} vs {v30c}) — manter o ritmo."),
            ("Vídeos longos rendem muito abaixo dos Shorts — parte da produção quase não performa." if sh_pct>=se_pct else "Alcance concentrado em busca; falta descoberta por Shorts/recomendação."),
            f"Conversão a inscrito: {sg(subs)} líquidos (~{round(subs/v30*100,1) if v30 else 0}% das views), vs {sg(subsc)} no período anterior."],
          "frequencia":"Manter 1 Short/dia e ajustar vídeos longos para 2–3/semana, focando no gancho e na retenção dos Shorts.",
          "conteudo":f"Priorizar os formatos do Top vídeos (ex.: '{top1}')"+(f" e temas de busca ('{bus[0][0]}')." if bus else "."),
          "estrategia":"Padronizar Shorts com o termo/sintoma exato no título e nos 3s iniciais, e usar o final para levar ao vídeo longo — convertendo o tráfego de Shorts em inscritos e watch time.",
          "atencao":(f"A queda de {abs(ch)}% no mês é o alerta central. " if ch<0 else "Manter a consistência é o desafio. ")+"O alcance depende de picos de Shorts e a conversão a inscrito é baixa — criar um funil para inscrição é o foco."}

def main():
    # ref a partir do 1º canal
    ref = detect_ref(build("youtubeAnalytics","v2",credentials=creds_for(CANAIS[0][0])))
    print("Último dia com dados (referência):", ref)
    data={}
    for tok,key,cls,cor,col in CANAIS:
        print("Buscando", key, "...")
        j=fetch(tok,ref)
        j.update({"cls":cls,"cor":cor,"col":col,"plano":build_plano(j)})
        data[key]=j
    ref_win=data[CANAIS[0][1]]["_win"]
    meta={
      "30d":{"label":"Últimos 30 dias","atual":{"inicio":ref_win["30d"][0],"fim":ref_win["30d"][1]},"comp":{"inicio":ref_win["30dc"][0],"fim":ref_win["30dc"][1]}},
      "7d":{"label":"Últimos 7 dias","atual":{"inicio":ref_win["7d"][0],"fim":ref_win["7d"][1]},"comp":{"inicio":ref_win["7dc"][0],"fim":ref_win["7dc"][1]}},
      "dia":{"label":"Dia mais recente","atual":{"inicio":ref_win["dia"][0],"fim":ref_win["dia"][1]},"comp":{"inicio":ref_win["diac"][0],"fim":ref_win["diac"][1]}},
    }
    def chan(j):
        return {"nome":j["nome"],"cls":j["cls"],"cor":j["cor"],"col":j["col"],
                "inscritos":j["inscritos"],"views_total":j["views_total"],"videos":j["videos"],
                "pend_long":j["pend_long"],"pend_short":j["pend_short"],
                "per":j["per"],"buscas":j["buscas"],"pub":j["pub"],"plano":j["plano"]}
    DADOS={"gerado_em":date.today().isoformat(),"ref":ref,"periodos_meta":meta,
           "canais":{k:chan(data[k]) for k in data}}
    # daily [MMDD,Hs,Hv,Ps,Pv] e dagg {MMDD:{p:[],h:[]}}
    dmap={}; dg={}
    for key,j in data.items():
        cls=j["cls"]
        for row in j["daily_type"]:
            dmap.setdefault(row[0],{})[cls]=(row[1],row[2])
        for day,val in j["dagg"].items():
            dg.setdefault(day,{})[cls]=val
    daily=[]
    for day in sorted(dmap):
        row=[day]+[0]*(2*len(data))
        for key,j in data.items():
            col=j["col"]; st=dmap[day].get(j["cls"],(0,0))
            row[col["short"]]=st[0]; row[col["long"]]=st[1]
        daily.append(row)
    dmin=ref_win["30d"][0]; dmax=ref

    dados_js="const DADOS="+json.dumps(DADOS,ensure_ascii=False)+";"
    daily_js="const daily="+json.dumps(daily)+";"
    dagg_js="const dagg="+json.dumps(dg,ensure_ascii=False)+";"

    h=open(HTML,encoding="utf-8").read()
    h,n1=re.subn(r"const DADOS=.*?(?=\n// diária)",dados_js,h,count=1,flags=re.DOTALL)
    h,n2=re.subn(r"const daily=.*?(?=\n// retenção)",daily_js,h,count=1,flags=re.DOTALL)
    h,n3=re.subn(r"const dagg=.*?(?=\nconst DMIN)",dagg_js,h,count=1,flags=re.DOTALL)
    h,n4=re.subn(r'min="\d{4}-\d{2}-\d{2}" max="\d{4}-\d{2}-\d{2}"',f'min="{dmin}" max="{dmax}"',h)
    h,n5=re.subn(r'const DMIN="[^"]*",DMAX="[^"]*";',f'const DMIN="{dmin}",DMAX="{dmax}";',h)
    h,n6=re.subn(r"let cIni='[^']*',cFim='[^']*';",f"let cIni='{dmin}',cFim='{dmax}';",h)
    if "ANNOTATION:'Anotações'" not in h:
        h=h.replace("NOTIFICATION:'Notificações'};","NOTIFICATION:'Notificações',ANNOTATION:'Anotações',SHORTS_CONTENT_LINKS:'Links de Shorts'};")
    open(HTML,"w",encoding="utf-8").write(h)
    print(f"OK -> DADOS={n1} daily={n2} dagg={n3} datas_input={n4} DMIN={n5} cIni/cFim={n6}")
    print(f"Janela: {dmin} a {dmax} | dias diários: {len(daily)}")
    for k,j in data.items():
        print(f"  {j['nome']}: 30d views={j['per']['30d']['a']['views']} | insc={j['inscritos']} | posts30d(v/s)={sum(1 for x in j['pub'] if ref_win['30d'][0][5:]<=x[0]<=ref_win['30d'][1][5:] and x[1]=='v')}/{sum(1 for x in j['pub'] if ref_win['30d'][0][5:]<=x[0]<=ref_win['30d'][1][5:] and x[1]=='s')}")

if __name__ == "__main__":
    main()
