#!/usr/bin/env python3
import json, re, os

BASE="/home/dev/.claude/projects/-opt-projects--dashboard-cinik-worktrees-tom-auto-populate/5c79ced8-8c91-47a0-a9c5-54e5c5f0ee15/tool-results"
FILES_WRAPPED={
 'dr-cinik': f"{BASE}/mcp-claude_ai_Ahref-site-explorer-all-backlinks-1780853366112.txt",
 'dr-serkan-aygin': f"{BASE}/mcp-claude_ai_Ahref-site-explorer-all-backlinks-1780853366079.txt",
 'cosmedica': f"{BASE}/mcp-claude_ai_Ahref-site-explorer-all-backlinks-1780853369017.txt",
 'vera-clinic': f"{BASE}/mcp-claude_ai_Ahref-site-explorer-all-backlinks-1780853371962.txt",
}
FILES_PLAIN={
 'clinicana': "/tmp/cinik-pull/clinicana-raw.json",
 'smile-hair-clinic': "/tmp/cinik-pull/smile-raw.json",
}

def load_wrapped(p):
    raw=json.load(open(p)); return json.loads(raw[0]['text'])['backlinks']
def load_plain(p):
    return json.load(open(p))['backlinks']

# ---- Editorial filter ----
SPAM_TLD_TOKENS=re.compile(r'(seo|link|backlink|rank|authority|dofollow)', re.I)
REVIEW_TPL=re.compile(r'there was a time last year', re.I)
# Known junk domains (directories, similarity, redirect/login/ugc wrappers)
JUNK_DOMAINS={
 'sitelike.org','siteprice.org','technofizi.net','publicwww.com','sitewideanalytics.com',
 'freelistingusa.com','askmap.net','fyple.co.uk','place123.net','todosbiz.es','sektor.gen.tr',
 'yandex.az','yandex.com','yandex.ru','yango.com','safariclub.org','zdn.vn','mints.ne.jp',
 'boardhost.com','pastelink.net','diigo.com','brunch.co.kr','clan.su','luma.com',
 'thesocietypages.org','lonestar.edu','usac.edu.gt','plesk.page','co.com','opovo.com.br',
 'neuthema.de','wbez.org','leasefintech.com','moneyfx.boardhost.com','finishedtask.com',
 'thefrisky.com','hairlinetransplantation.com',
 # similarity / aggregator / freehost / paste / bookmark / generic junk seen in wrapped files
 'web.app','page.tl','figma.site','ghost.io','figma.com','glarity.app','parse.gl',
 'video-bookmark.com','joinrunway.io','vuink.com','2ch.org','d-h.st','xpr.media',
 'yandex.md','yandex.uz','yandex.com.tr','moviezone.cz','stadt1.de','abc-directory.com',
 'insightmag.com','epubzone.org','dowjones.io','minyanville.com','sourcefed.com',
 'flokii.com','flokib.at','bizlistusa.com','bizratings.com','yplocal.com','citysquares.com',
 'connectively.us','synara.ar','am-news.com','marketminute.com',
}
# Syndicated-PR mirror network: td==0 (or tiny) news-mirror domains republishing one wire headline.
# These are PBN-like syndication mirrors, not genuine editorial.
PR_MIRROR_TITLE=re.compile(
 r'(Vera Clinic and Appsilon|Bio-Authenticity Premium|Becomes Official Global Partner|'
 r'Official Partner of the Houston Rockets|Unveil Vector 10|Fixed-Price Hair Transplant Packages|'
 r'Top 5 Hair Transplant Clinics in Turkey: Leading Providers|'
 r'Cliniques de Greffe de Cheveux en Turquie . Guide Sp)', re.I)
# Mass guest-post farm: traffic_domain 0, DR ~68-76, generic invented brand + spun title
GUESTPOST_TITLE=re.compile(r'(hair transplant turkey|hair transplant istanbul|best hair transplant|hair transplant turkey)', re.I)

def domain_is_pbn_shop(dom):
    if dom.endswith('.shop') or dom.endswith('.store'):
        return True
    return False

def is_excluded(b):
    dom=b.get('root_name_source','') or ''
    title=b.get('title','') or ''
    td=b.get('traffic_domain',0) or 0
    dr=b.get('domain_rating_source',0) or 0
    if b.get('is_ugc'): return True,'ugc'
    if domain_is_pbn_shop(dom): return True,'pbn-shop'
    if REVIEW_TPL.search(title): return True,'review-tpl'
    if dom in JUNK_DOMAINS: return True,'junk-domain'
    # syndication "User |" profile/author junk pages
    if title.startswith('User |') or title.startswith('User ') and '|' in title[:8]:
        return True,'user-profile-junk'
    # syndicated-PR mirror network: recognizable wire headline republished across mirror domains.
    # Keep only genuine PR-wire originators (einnews, ipsnews, newsblaze, issuewire); drop the rest.
    PR_ORIGINATORS={'einnews.com','ipsnews.net','newsblaze.com','issuewire.com',
                    'globenewswire.com','24-7pressrelease.com','natlawreview.com',
                    'presseportal.de','presseportal.ch','ots.at'}
    if PR_MIRROR_TITLE.search(title) and dom not in PR_ORIGINATORS:
        return True,'pr-mirror'
    # Off-topic incidental links on real outlets (manually reviewed): page not about the brand,
    # hair transplant, or aesthetic medicine -> not brand editorial.
    OFFTOPIC_DOMAINS={'tastingtable.com','ladbible.com','ctinsider.com','gathered.how',
                      'yardbarker.com','pastemagazine.com','patient.info','audioboom.com',
                      'factually.co','morecore.de','nona.my','lifecoachcode.com','lifepositive.com',
                      'sport.fr','darelhilal.com','2ch.org','moviezone.cz'}
    if dom in OFFTOPIC_DOMAINS: return True,'offtopic-incidental'
    # generic news-mirror clone domains (newswatch/newsonline/gazette/worldreport/times/daily etc.)
    # republishing the same story with traffic_domain 0 -> syndication PBN
    if td==0 and re.search(r'(newswatch|newsonline|newsnetwork|nouvelles|gazette|worldreport|'
                           r'industrytoday|bizsense|dailynews|daily-news|times-online|phenomena|'
                           r'pluralist|mmminimal|sausalito|fictiontalk|thedishh|small-biz|'
                           r'newscenter|travelindustry)', dom, re.I):
        return True,'news-mirror'
    # edu.pl / .id / .nl / .org clones with spun titles & traffic_domain 0
    if td==0 and GUESTPOST_TITLE.search(title): return True,'guestpost-farm'
    # generic invented-brand .com with traffic_domain 0 and spun hair-transplant title
    if td==0 and re.search(r'\b(2026|2027)\b', title) and GUESTPOST_TITLE.search(title):
        return True,'guestpost-farm'
    return False,None

# ---- Classify ----
PR_DOMAINS={'globenewswire.com','presseportal.de','presseportal.ch','ots.at','natlawreview.com',
            'einnews.com','abnewswire.com','prnewswire.com','businesswire.com','ipsnews.net',
            'newsblaze.com','24-7pressrelease.com','issuewire.com','financialcontent.com',
            'marketminute.com','ftnnews.com'}
REAL_MEDIA={'ouest-france.fr','ledauphine.com','scotsman.com','yorkshirepost.co.uk',
            'derwesten.de','ga.de','volksfreund.de','oe24.at','gulfnews.com','targatocn.it',
            'iprima.cz','andaluciainformacion.es','nyweekly.com','intermediatv.ro',
            'businessinsider.com','20minutes.fr','westword.com','muscleandfitness.com',
            'baltimoresun.com','ctinsider.com','ladbible.com','vanguardia.com','epoznan.pl',
            'newcastleherald.com.au','corkbeo.ie','cultmtl.com','sunraysiadaily.com.au',
            'observer-reporter.com','tastingtable.com','yardbarker.com','pastemagazine.com',
            'index.hr','sport.fr','traveldailynews.com','podkarpacielive.pl','gigwise.com',
            'lifeandstylemag.com','varsity.co.uk','oberberg-aktuell.de','buzzwebzine.fr',
            'gathered.how','ma-grande-taille.com','viepratique.fr','les-docus.com',
            'copenhagenfashionsummit.com','yourhealthmagazine.net','primalamartesana.it',
            'radiortm.it','termometropolitico.it','cicloweb.it','forschung-und-wissen.de',
            'theexeterdaily.co.uk','emilyluxton.co.uk','barbedudaron.fr','vocal.media',
            'paperblog.com'}
BRAND_PROFILE={'qanomed.com','aslitarcanglobal.com','sapphirehairclinic.com','provenexpert.com',
               'alopezie.de','drcinik.com','charlesmedicalgroup.com','shapiromedical.com',
               'bestclinic.co.uk','traya.health','blue-print.co','myhair.ai','verasmile.com',
               'bizzr.uk','hashtagbeauty.de','meinhaushalt.at'}
def classify(b):
    dom=b.get('root_name_source','') or ''
    url=b.get('url_from','') or ''
    if dom in PR_DOMAINS: return 'press_release'
    if b.get('is_sponsored') or '/sponsored/' in url or '/paroles-de-partenaires/' in url or '/friday-partner/' in url:
        return 'sponsored_article'
    if dom in REAL_MEDIA: return 'advertorial'
    if dom in BRAND_PROFILE: return 'other'
    return 'guest_post'

# ---- Country mapping ----
TLD_COUNTRY={'fr':'FR','de':'DE','it':'IT','es':'ES','cz':'CZ','at':'AT','ch':'CH','co.uk':'GB',
             'uk':'GB','ro':'RO','com.br':'BR','ne.jp':'JP','jp':'JP','kr':'KR','co.kr':'KR',
             'gen.tr':'TR','tr':'TR','az':'AZ','su':'RU','nl':'NL','gt':'GT','pl':'PL','id':'ID',
             'vn':'VN'}
LANG_COUNTRY={'fr':'FR','de':'DE','it':'IT','es':'ES','cs':'CZ','pt':'BR','ko':'KR','ja':'JP',
              'tr':'TR','ru':'RU','ar':None,'id':'ID','ro':'RO','nl':'NL','sv':'SE','en':None}
def country_of(b):
    dom=b.get('root_name_source','') or ''
    parts=dom.split('.')
    # try two-level tld then one-level
    for n in (2,1):
        suf='.'.join(parts[-n:])
        if suf in TLD_COUNTRY: return TLD_COUNTRY[suf]
    if parts and parts[-1] in TLD_COUNTRY: return TLD_COUNTRY[parts[-1]]
    langs=b.get('languages') or []
    if langs:
        c=LANG_COUNTRY.get(langs[0])
        if c: return c
        if langs[0]=='en': return 'GB'
    return None

INT32_MAX=2147483647
def transform(b):
    traffic=b.get('traffic',0) or 0
    td=b.get('traffic_domain',0) or 0
    langs=b.get('languages') or []
    eff_traffic=traffic if traffic>0 else td
    if eff_traffic>INT32_MAX: eff_traffic=INT32_MAX  # DB column is 32-bit Int
    return {
        'url': b['url_from'],
        'domain': b['root_name_source'],
        'domain_rating': int(round(b.get('domain_rating_source') or 0)),
        'traffic': eff_traffic,
        'country': country_of(b),
        'language': langs[0] if langs else None,
        'category': classify(b),
        'is_dofollow': bool(b.get('is_dofollow')),
        'first_seen': (b.get('first_seen_link') or '')[:10],
        'comment': "",
    }

TARGETS={'dr-cinik':'emrahcinik.com','dr-serkan-aygin':'drserkanaygin.com',
         'cosmedica':'cosmedica.com','vera-clinic':'veraclinic.net',
         'clinicana':'clinicana.com','smile-hair-clinic':'smilehairclinic.com'}

summary={}
for slug,loader,path in (
    [(s,load_wrapped,p) for s,p in FILES_WRAPPED.items()] +
    [(s,load_plain,p) for s,p in FILES_PLAIN.items()]):
    bl=loader(path)
    raw_count=len(bl)
    kept=[]
    seen_urls=set(); seen_domains=set()
    excl=0
    for b in bl:
        ex,reason=is_excluded(b)
        if ex: excl+=1; continue
        url=b['url_from']; dom=b['root_name_source']
        if url in seen_urls: continue
        if dom in seen_domains: continue
        seen_urls.add(url); seen_domains.add(dom)
        kept.append(transform(b))
    # category breakdown
    cats={}
    for k in kept: cats[k['category']]=cats.get(k['category'],0)+1
    # top placements
    top=sorted(kept,key=lambda x:-x['domain_rating'])[:3]
    out={'meta':{'target':TARGETS[slug],'period':'2026-05','total':len(kept)},'backlinks':kept}
    with open(f"/tmp/cinik-bl-{slug}.json",'w') as f:
        json.dump(out,f,ensure_ascii=False,indent=1)
    summary[slug]={'raw':raw_count,'excluded':excl,'kept':len(kept),'cats':cats,
                   'top':[(t['domain'],t['domain_rating']) for t in top]}

print(json.dumps(summary,ensure_ascii=False,indent=2))
