"""The Qawlak side of the free-Resolve helper (docs/plugins/resolve-helper): resolve.json, the stdlib HTTP client and the
editor-facing message words. Stdlib only.

The Resolve Studio pieces that used to live here (external scripting import/roundtrip, the Python menu twins and the
Workflow Integration skeleton) were removed on 16 Sep 2026 (R29, Karan): nothing on this PC can run them. Free Resolve's
menu scripts are Lua (resolve-menu/), and they hand files to the helper, which uses this module.
"""
import json
import math
import os
import time
import urllib.error
import urllib.request
from pathlib import Path


ARABIC_DIALECTS = ('auto', 'gulf', 'egyptian', 'levantine', 'maghrebi', 'msa')


def dialect_for(language, dialect):
    """The server preset (resumable.DIALECTS): Hindi and Urdu decode through their own presets, like the Premiere panels (P29);
    Arabic takes the dialect resolve.json names; anything else is auto. E37: English decodes through its own preset too."""
    if language in ('hi', 'ur', 'en'): return {'hi': 'hindi', 'ur': 'urdu', 'en': 'english'}[language]
    return dialect if language == 'ar' and dialect in ARABIC_DIALECTS else 'auto'


def plugin_config(env=None, home=None):
    """Key and server: env wins, then resolve.json in %APPDATA%\\Qawlak (~/.config/qawlak off Windows). The file holds a
    plugin key only (upload/transcribe/export, P5-07), in the user's own profile; revoke it from Qawlak > Plugins."""
    env = os.environ if env is None else env
    base = Path(env['APPDATA']) / 'Qawlak' if env.get('APPDATA') else Path(home or Path.home()) / '.config' / 'qawlak'
    path = base / 'resolve.json'
    try: data = json.loads(path.read_text('utf-8-sig')) if path.is_file() else {}   # -sig: a Notepad-saved file has a BOM
    except ValueError: raise SystemExit(f'{path} is not valid JSON.')
    return {'key': env.get('QAWLAK_KEY') or data.get('key'), 'url': env.get('QAWLAK_URL') or data.get('url') or 'http://127.0.0.1:8765',
            'language': data.get('language', 'ar'), 'dialect': dialect_for(data.get('language', 'ar'), data.get('dialect')),
            'alpha': bool(data.get('alpha')), 'path': path}


class QawlakError(SystemExit):
    """A non-2xx answer: still a SystemExit for the scripts, with the status and JSON body for the helper's messages."""
    def __init__(self, message, status=None, body=None):
        super().__init__(message)
        self.status, self.body = status, body or {}


# P15-43 / P15-45 / D15-10 messages, the same words and rounding as Qawlak Import.lua's (tests assert identical strings),
# for the resolve-helper's notifications. The Lua copy stays for the Console round trip, which cannot import Python.
AR_DIGITS = str.maketrans('0123456789.', '٠١٢٣٤٥٦٧٨٩٫')


def minutes(seconds, up):
    m = (float(seconds or 0)) / 60
    if m >= 10: return math.ceil(m - 1e-9) if up else math.floor(m + 1e-9)
    return (math.ceil(m * 10 - 1e-9) if up else math.floor(m * 10 + 1e-9)) / 10


def minutes_phrase(value, language):
    text = str(int(value)) if value == int(value) else str(value)
    if language != 'ar': return text + ' min'
    if value == 1: return 'دقيقة واحدة'
    if value == 2: return 'دقيقتان'
    return text.translate(AR_DIGITS) + (' دقائق' if value == int(value) and 3 <= value <= 10 else ' دقيقة')


def join_url(base, path):
    return path if path.startswith(('http://', 'https://')) else (base or '').rstrip('/') + path


def low_balance_message(ping, base, language):
    if not ping.get('low_balance'): return None
    left = minutes(ping['seconds_left'], False) if ping.get('seconds_left') is not None else float(ping.get('minutes_left') or 0)
    url = join_url(base, ping.get('buy_url') or '/pricing.html#packs')
    if language == 'ar': return f'قولك: المتبقي {minutes_phrase(left, "ar")} · اشترِ باقة من {url}'
    return f'Qawlak: {minutes_phrase(left, "en")} left · buy a pack at {url}'


def credit_wall_message(body, base, language):
    need, have = minutes(body.get('needed_seconds'), True), minutes(body.get('available_seconds'), False)
    url = join_url(base, body.get('buy_url') or '/pricing.html#packs')
    if language == 'ar':
        return f'قولك: الرصيد لا يكفي لهذا الملف: المطلوب {minutes_phrase(need, "ar")} والمتبقي {minutes_phrase(have, "ar")} · اشترِ باقة من {url}'
    return f'Qawlak: not enough minutes for this file: {minutes_phrase(need, "en")} needed, {minutes_phrase(have, "en")} left · buy a pack at {url}'


def refusal_message(body, language):
    detail = (body or {}).get('detail')
    if not isinstance(detail, str): return None
    ar, sep, en = detail.partition(' / ')
    if not sep: return 'Qawlak: ' + detail
    return 'قولك: ' + ar if language == 'ar' else 'Qawlak: ' + en


class Api:
    def __init__(self, url, key):
        self.url, self.key = url.rstrip('/'), key

    def call(self, method, path, body=None, data=None, params=None):
        url = self.url + path + ('?' + '&'.join(f'{k}={v}' for k, v in params.items()) if params else '')
        headers = {'Authorization': 'Bearer ' + self.key}
        if body is not None: data, headers['Content-Type'] = json.dumps(body).encode(), 'application/json'
        elif data is not None: headers['Content-Type'] = 'application/octet-stream'
        try:
            with urllib.request.urlopen(urllib.request.Request(url, data=data, method=method, headers=headers), timeout=600) as r:
                raw = r.read(); ctype = r.headers.get('Content-Type', '')
        except urllib.error.HTTPError as e:
            raw = e.read()
            try: body = json.loads(raw)
            except ValueError: body = None
            body = body if isinstance(body, dict) else {}
            detail = body.get('detail')   # P15-45: the server's reason (e.g. an API key below Studio), not a raw body
            raise QawlakError(f'Qawlak: {detail}' if isinstance(detail, str) else f'{method} {path} -> {e.code}: {raw[:300].decode(errors="replace")}',
                              e.code, body) from e
        return json.loads(raw) if 'json' in ctype else raw

    def upload(self, path, dialect=None):
        path = Path(path); size = path.stat().st_size
        session = self.call('POST', '/api/uploads', {'name': path.name, 'size': size})
        with path.open('rb') as f:
            offset = 0
            while offset < size:
                f.seek(offset); chunk = f.read(session['chunk_size'])
                offset = self.call('PATCH', '/api/uploads/' + session['id'], data=chunk, params={'offset': offset})['offset']
        # ?dialect= makes the job the upload starts by itself use the preset, so transcribe joins it instead of meeting a
        # running Arabic job with 409 (the P29 Hindi/Urdu failure).
        params = {'dialect': dialect} if dialect and dialect != 'auto' else None
        return self.call('POST', '/api/uploads/' + session['id'] + '/complete', params=params)

    def job(self, pid, path, body=None, timeout=3600, post=True):
        """Queue a job (post=False: one already queued, e.g. after a dropped connection) and poll until it is ready."""
        if post: self.call('POST', f'/api/projects/{pid}{path}', body or {})
        deadline = time.monotonic() + timeout
        while time.monotonic() < deadline:
            p = self.call('GET', f'/api/projects/{pid}')
            if p['status'] == 'error': raise SystemExit(p.get('error') or 'Qawlak job failed')
            if p['status'] == 'ready': return p
            time.sleep(2)
        raise SystemExit('Qawlak is still processing; open the project in the studio later.')
