#!/usr/bin/env python3
"""Check a CSV of group,language,url. Python 3 standard library, no installation.
Usage: python check_hreflang.py hreflang-map.csv > result.json
This fetches only the URLs supplied in your CSV. Run only against sites you may audit.
HTML-response check, not a browser rendering or Google indexing test.
"""
import csv
import json
import sys
from datetime import datetime, timezone
from html.parser import HTMLParser
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError
from urllib.parse import urlparse


class Head(HTMLParser):
    def __init__(self):
        super().__init__()
        self.canonical, self.alternates, self.robots = [], {}, []
    def handle_starttag(self, tag, attrs):
        a = dict(attrs)
        if tag == 'link' and 'canonical' in a.get('rel', '').split():
            self.canonical.append(a.get('href'))
        if tag == 'link' and 'alternate' in a.get('rel', '').split() and a.get('hreflang'):
            self.alternates.setdefault(a['hreflang'], []).append(a.get('href'))
        if tag == 'meta' and a.get('name', '').lower() in ('robots', 'googlebot'):
            self.robots.append(a.get('content', '').lower())


def inspect(row, expected):
    url = row['url']
    errors = []
    result = {'group': row['group'], 'language': row['language'], 'url': url}
    try:
        if urlparse(url).scheme != 'https':
            raise ValueError('Only absolute HTTPS URLs are accepted')
        with urlopen(Request(url, headers={'User-Agent': 'Ramfa-Hreflang-Check/1.0'}), timeout=20) as response:
            result.update(status=response.status, final_url=response.url)
            head = Head()
            head.feed(response.read(2000000).decode('utf-8', errors='replace'))
            if response.url != url: errors.append('URL redirects')
            if head.canonical != [url]: errors.append('Expected one self-canonical')
            if any('noindex' in rule or 'none' == rule.strip() for rule in head.robots) or 'noindex' in response.headers.get('X-Robots-Tag', '').lower(): errors.append('Noindex')
            for language, target in expected.items():
                if head.alternates.get(language) != [target]: errors.append('Missing, duplicate or wrong alternate: '+language)
            for language in head.alternates:
                if language not in expected: errors.append('Unexpected alternate: '+language)
            result.update(canonical=head.canonical, alternates=head.alternates)
    except (HTTPError, URLError, ValueError, TimeoutError) as error:
        result['status'] = getattr(error, 'code', None)
        errors.append(str(error))
    result.update(ok=not errors, errors=errors)
    return result


def run(filename):
    with open(filename, encoding='utf-8-sig', newline='') as source:
        rows = list(csv.DictReader(source))
    if not rows or not {'group', 'language', 'url'} <= rows[0].keys():
        raise ValueError('CSV needs group,language,url columns')
    groups = {}
    for row in rows:
        group = groups.setdefault(row['group'], {})
        if row['language'] in group: raise ValueError('Duplicate language in '+row['group'])
        group[row['language']] = row['url']
    results = []
    for row in rows:
        expected = dict(groups[row['group']])
        # This site's documented fallback is English. Adapt if your policy differs.
        if 'en' in expected: expected['x-default'] = expected['en']
        results.append(inspect(row, expected))
    return {'checked_at': datetime.now(timezone.utc).isoformat(), 'method': 'Initial HTML, reciprocal groups from CSV',
            'count': len(results), 'passed': sum(r['ok'] for r in results), 'results': results}


if __name__ == '__main__':
    report = run(sys.argv[1])
    print(json.dumps(report, ensure_ascii=False, indent=2))
    sys.exit(0 if report['count'] == report['passed'] else 1)
