"""مزامنة مصاريف الإعلانات من Meta Marketing API و Google Ads API.

الملفات المستخدمة:
  ads_rows.json      - صفوف خام: {platform: {date: [{id, name, spend}]}}
  manual_spend.json  - صرف يدوي: {date: {product: amount}}
  ad_spend.json      - الناتج المجمّع اللي الواجهة بتقرأه: {date: {product: amount}}
  ads_state.json     - حالة آخر مزامنة لكل منصة
"""

import calendar
import csv
import io
import json
import os
import re
import ssl
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime

DATA_DIR = os.path.dirname(os.path.abspath(__file__))
CONFIG_FILE = os.path.join(DATA_DIR, "config.json")
ROWS_FILE = os.path.join(DATA_DIR, "ads_rows.json")
LINES_FILE = os.path.join(DATA_DIR, "ad_spend_lines.json")
MANUAL_FILE = os.path.join(DATA_DIR, "manual_spend.json")
AGG_FILE = os.path.join(DATA_DIR, "ad_spend.json")
STATE_FILE = os.path.join(DATA_DIR, "ads_state.json")
CAMPAIGNS_FILE = os.path.join(DATA_DIR, "ads_campaigns.json")
ORDERS_CSV = os.path.join(DATA_DIR, "orders.csv")

PRODUCTS = ['نيو بوما', 'بوما Puma', 'كوتشي إم', 'كوتشي ميجا', 'أخرى']
PLATFORMS = ('meta', 'google')

_SSL = ssl.create_default_context()
_SSL.check_hostname = False
_SSL.verify_mode = ssl.CERT_NONE


class AdsError(Exception):
    """خطأ مفهوم للمستخدم من أي منصة إعلانات."""


# --------------------------------------------------------------------------
# أدوات ملفات
# --------------------------------------------------------------------------

def _read_json(path, default):
    if not os.path.exists(path):
        return default
    try:
        with open(path, 'r', encoding='utf-8') as f:
            data = json.load(f)
        return data if isinstance(data, type(default)) else default
    except Exception:
        return default


def _write_json(path, data):
    tmp = path + '.tmp'
    with open(tmp, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=2)
    os.replace(tmp, path)


def normalize_product(name):
    """توحيد أسماء المنتجات القديمة/المختصرة إلى الأسماء القياسية."""
    if not name:
        return 'أخرى'
    key = name.strip()
    aliases = {
        'puma': 'بوما Puma', 'Puma': 'بوما Puma', 'بوما': 'بوما Puma',
        'كوتشي بوما': 'بوما Puma', 'بوما كلي': 'بوما Puma',
        'm': 'كوتشي إم', 'M': 'كوتشي إم', 'M شوز': 'كوتشي إم',
        'كوتشي ام': 'كوتشي إم', 'كوتشي إم': 'كوتشي إم',
        'ميجا': 'كوتشي ميجا', 'كوتشي ميجا': 'كوتشي ميجا',
        'نيو بوما': 'نيو بوما', 'كوتشي نيو بوما': 'نيو بوما',
        'أخرى': 'أخرى', 'اخرى': 'أخرى',
    }
    return aliases.get(key, key)


def classify_campaign(name):
    """تخمين المنتج من اسم الحملة أو المجموعة الإعلانية."""
    n = (name or '').lower()
    if 'ميجا' in n or 'mega' in n:
        return 'كوتشي ميجا'
    if 'نيو بوما' in n or 'new puma' in n:
        return 'نيو بوما'
    if 'بوما' in n or 'بيوما' in n or 'puma' in n:
        return 'بوما Puma'
    if 'm شوز' in n or 'm shoes' in n or 'ام شوز' in n or 'إم شوز' in n:
        return 'كوتشي إم'
    if re.search(r'كوتشي\s*m\b', n) or re.search(r'\bm\b', n):
        return 'كوتشي إم'
    return 'أخرى'


def month_to_range(month):
    """'11-2026' -> ('2026-11-01', '2026-11-30')."""
    try:
        m, y = month.split('-')
        m, y = int(m), int(y)
        last = calendar.monthrange(y, m)[1]
        return f"{y}-{m:02d}-01", f"{y}-{m:02d}-{last:02d}"
    except Exception:
        raise AdsError("صيغة الشهر غير صحيحة، المتوقع مثل 11-2026")


def norm_date(value):
    """توحيد أي تاريخ إلى صيغة اللوحة: DD-MM-YYYY."""
    value = (value or '').strip()
    for fmt in ('%Y-%m-%d', '%d-%m-%Y', '%d/%m/%Y'):
        try:
            return datetime.strptime(value, fmt).strftime('%d-%m-%Y')
        except ValueError:
            continue
    return value


def default_range():
    """كل الفترة المتاحة في الأوردرات."""
    try:
        from csv import DictReader
        with open(ORDERS_CSV, 'r', encoding='utf-8-sig') as f:
            dates = [r.get('تاريخ الإنشاء', '') for r in DictReader(f)]
        parsed = [datetime.strptime(d, '%d-%m-%Y') for d in dates if d]
        if parsed:
            return min(parsed).strftime('%Y-%m-%d'), max(parsed).strftime('%Y-%m-%d')
    except Exception:
        pass
    today = datetime.now()
    return today.replace(day=1).strftime('%Y-%m-%d'), today.strftime('%Y-%m-%d')


# --------------------------------------------------------------------------
# HTTP
# --------------------------------------------------------------------------

def _http(url, method='GET', headers=None, body=None, form=None, timeout=30):
    data = None
    hdrs = {'User-Agent': 'EasyOrdersDashboard/1.0'}
    if form is not None:
        data = urllib.parse.urlencode(form).encode()
        hdrs['Content-Type'] = 'application/x-www-form-urlencoded'
    elif body is not None:
        data = json.dumps(body).encode()
        hdrs['Content-Type'] = 'application/json'
    hdrs.update(headers or {})

    req = urllib.request.Request(url, data=data, method=method, headers=hdrs)
    try:
        with urllib.request.urlopen(req, context=_SSL, timeout=timeout) as resp:
            raw = resp.read().decode('utf-8', 'replace')
    except urllib.error.HTTPError as e:
        raw = e.read().decode('utf-8', 'replace')
        raise AdsError(_extract_error(raw) or f"فشل الطلب ({e.code})")
    except urllib.error.URLError as e:
        raise AdsError(f"تعذر الاتصال بالمنصة: {e.reason}")
    except Exception as e:
        raise AdsError(f"خطأ في الاتصال: {e}")

    if not raw:
        return {}
    try:
        return json.loads(raw)
    except ValueError:
        raise AdsError(f"رد غير مفهوم من المنصة: {raw[:200]}")


def _extract_error(raw):
    """رسالة الخطأ من رد ميتا أو جوجل."""
    try:
        payload = json.loads(raw)
    except Exception:
        return raw[:300].strip()
    if isinstance(payload, list) and payload:
        payload = payload[0]
    if isinstance(payload, dict):
        err = payload.get('error')
        if isinstance(err, dict):
            return err.get('message') or err.get('error_user_msg')
        return payload.get('message')
    return None


def _action_map(actions):
    result = {}
    for item in actions or []:
        try:
            key = item.get('action_type')
            result[key] = result.get(key, 0.0) + float(item.get('value') or 0)
        except (TypeError, ValueError, AttributeError):
            continue
    return result


def _to_int(value):
    try:
        return int(float(value or 0))
    except (TypeError, ValueError):
        return 0


# --------------------------------------------------------------------------
# Meta (Facebook / Instagram Ads)
# --------------------------------------------------------------------------

class MetaAds:
    def __init__(self, account_id, access_token, version='v21.0'):
        account_id = (account_id or '').strip()
        if not account_id:
            raise AdsError("أدخل Ad Account ID (act_...) أولاً")
        if not account_id.startswith('act_'):
            account_id = 'act_' + account_id
        self.account_id = account_id
        self.token = (access_token or '').strip()
        self.version = version or 'v21.0'
        if not self.token:
            raise AdsError("أدخل الـ Access Token أولاً")

    def _url(self, path, params):
        params = dict(params)
        params['access_token'] = self.token
        base = f"https://graph.facebook.com/{self.version}/{path}"
        return f"{base}?{urllib.parse.urlencode(params)}"

    def account(self):
        data = _http(self._url(self.account_id, {
            'fields': 'name,account_status,currency,timezone_name'
        }))
        if 'error' in data:
            raise AdsError(_extract_error(json.dumps(data)))
        return data

    def daily_campaign_spend(self, since, until):
        params = {
            'level': 'campaign',
            'fields': 'campaign_id,campaign_name,spend,impressions,clicks,inline_link_clicks,'
                      'actions,action_values,date_start',
            'time_increment': 1,
            'time_range': json.dumps({'since': since, 'until': until}),
            'limit': 500,
        }
        url = self._url(f"{self.account_id}/insights", params)
        rows, pages = [], 0
        while url and pages < 50:
            pages += 1
            payload = _http(url)
            if 'error' in payload:
                raise AdsError(_extract_error(json.dumps(payload)))
            for r in payload.get('data', []):
                try:
                    spend = float(r.get('spend') or 0)
                except (TypeError, ValueError):
                    spend = 0.0
                d = r.get('date_start')
                if not d:
                    continue
                actions = _action_map(r.get('actions'))
                values = _action_map(r.get('action_values'))
                rows.append({
                    'date': norm_date(d),
                    'id': str(r.get('campaign_id') or ''),
                    'name': r.get('campaign_name') or '(حملة بدون اسم)',
                    'spend': round(spend, 2),
                    'impressions': _to_int(r.get('impressions')),
                    'clicks': _to_int(r.get('inline_link_clicks') or r.get('clicks')),
                    'lp_views': _to_int(actions.get('landing_page_view')),
                    'atc': _to_int(actions.get('add_to_cart')),
                    'checkouts': _to_int(actions.get('initiate_checkout')),
                    'purchases': _to_int(actions.get('purchase')),
                    'purchase_value': round(values.get('purchase', 0.0), 2),
                })
            url = (payload.get('paging') or {}).get('next')
        return rows


# --------------------------------------------------------------------------
# Google Ads
# --------------------------------------------------------------------------

class GoogleAds:
    def __init__(self, cfg):
        cfg = cfg or {}
        self.developer_token = (cfg.get('developer_token') or '').strip()
        self.customer_id = (cfg.get('customer_id') or '').replace('-', '').replace(' ', '')
        self.login_customer_id = (cfg.get('login_customer_id') or '').replace('-', '').replace(' ', '')
        self.client_id = (cfg.get('client_id') or '').strip()
        self.client_secret = (cfg.get('client_secret') or '').strip()
        self.refresh_token = (cfg.get('refresh_token') or '').strip()
        self.version = cfg.get('api_version') or 'v21'

        if not self.developer_token:
            raise AdsError("أدخل Developer Token من Google Ads API Center")
        if not self.customer_id:
            raise AdsError("أدخل Customer ID (10 أرقام بدون شرطات)")
        if not (self.client_id and self.client_secret and self.refresh_token):
            raise AdsError("بيانات OAuth ناقصة — شغّل google_oauth_setup.py الأول")

    def access_token(self):
        payload = _http(
            'https://oauth2.googleapis.com/token',
            method='POST',
            form={
                'grant_type': 'refresh_token',
                'client_id': self.client_id,
                'client_secret': self.client_secret,
                'refresh_token': self.refresh_token,
            },
        )
        token = payload.get('access_token')
        if not token:
            raise AdsError(_extract_error(json.dumps(payload)) or "تعذر تجديد توكن جوجل — أعد تشغيل google_oauth_setup.py")
        return token

    def search(self, query):
        token = self.access_token()
        headers = {
            'Authorization': f'Bearer {token}',
            'developer-token': self.developer_token,
        }
        if self.login_customer_id:
            headers['login-customer-id'] = self.login_customer_id

        url = (f"https://googleads.googleapis.com/{self.version}"
               f"/customers/{self.customer_id}/googleAds:search")
        results, page_token, pages = [], None, 0
        while pages < 50:
            pages += 1
            body = {'query': query}
            if page_token:
                body['pageToken'] = page_token
            payload = _http(url, method='POST', headers=headers, body=body)
            if isinstance(payload, list) and payload:
                raise AdsError(_extract_error(json.dumps(payload)))
            results.extend(payload.get('results') or [])
            page_token = payload.get('nextPageToken')
            if not page_token:
                break
        return results

    def daily_campaign_spend(self, since, until):
        query = (
            "SELECT campaign.id, campaign.name, segments.date, metrics.cost_micros "
            "FROM campaign "
            f"WHERE segments.date BETWEEN '{since}' AND '{until}' "
            "AND campaign.status != 'REMOVED'"
        )
        rows = []
        for item in self.search(query):
            metrics = item.get('metrics') or {}
            try:
                micros = int(metrics.get('costMicros') or 0)
            except (TypeError, ValueError):
                micros = 0
            d = (item.get('segments') or {}).get('date')
            spend = micros / 1_000_000
            if not d or spend <= 0:
                continue
            campaign = item.get('campaign') or {}
            rows.append({
                'date': norm_date(d),
                'id': str(campaign.get('id') or ''),
                'name': campaign.get('name') or '(حملة بدون اسم)',
                'spend': round(spend, 2),
            })
        return rows


# --------------------------------------------------------------------------
# التخزين والتجميع
# --------------------------------------------------------------------------

def load_rows():
    return _read_json(ROWS_FILE, {})


def load_manual():
    return _read_json(MANUAL_FILE, {})


def load_state():
    return _read_json(STATE_FILE, {})


def save_state(state):
    _write_json(STATE_FILE, state)


def enabled_sources(cfg):
    sources = cfg.get('spend_sources_enabled')
    if not isinstance(sources, list) or not sources:
        sources = ['manual']
    return [s for s in sources if s in ('meta', 'google', 'manual')]


def store_rows(platform, rows, cfg):
    """يحفظ الصفوف الخام لكل تاريخ ويعيد حساب جدول الحملات من جديد."""
    data = load_rows()
    platform_rows = data.setdefault(platform, {})
    by_date = {}
    for r in rows:
        r['date'] = norm_date(r['date'])
        by_date.setdefault(r['date'], []).append(r)
    for d, items in by_date.items():
        incoming_accounts = {r.get('account') for r in items}
        if incoming_accounts == {None}:
            platform_rows[d] = items
        else:
            # صفوف قديمة بلا حساب تُستبدل بأول مزامنة متعددة الحسابات
            kept = [r for r in platform_rows.get(d, [])
                    if r.get('account') is not None and r.get('account') not in incoming_accounts]
            platform_rows[d] = kept + items
    _write_json(ROWS_FILE, data)

    # إعادة بناء جدول الحملات من كل الصفوف المخزنة (بدون تكرار)
    campaigns = {}
    mapping = cfg.get('campaign_map') or {}
    for plat in PLATFORMS:
        for items in (data.get(plat) or {}).values():
            for r in items:
                key = f"{plat}:{r['id']}"
                item = campaigns.setdefault(key, {
                    'platform': plat, 'id': r['id'], 'name': r['name'],
                    'product': normalize_product(mapping.get(key)) if key in mapping
                    else classify_campaign(r['name']),
                    'spend': 0.0,
                })
                item['name'] = r['name']
                item['spend'] = round(item['spend'] + r['spend'], 2)
    _write_json(CAMPAIGNS_FILE, campaigns)
    return campaigns


def load_campaigns():
    return _read_json(CAMPAIGNS_FILE, {})


def campaign_product(key, name, cfg):
    mapping = cfg.get('campaign_map') or {}
    if key in mapping:
        return normalize_product(mapping[key])
    return classify_campaign(name)


def adgroup_key(platform, row):
    """مفتاح ربط المجموعة الإعلانية: google:<campaign_id>:<adgroup>."""
    if not row.get('adgroup'):
        return None
    return f"{platform}:{row.get('id')}:{row['adgroup']}"


def row_product(platform, row, cfg):
    """المنتج النهائي لصف: ربط المجموعة → ربط الحملة → تخمين من الاسم."""
    mapping = cfg.get('campaign_map') or {}
    ag_key = adgroup_key(platform, row)
    if ag_key and ag_key in mapping:
        return normalize_product(mapping[ag_key])
    camp_key = f"{platform}:{row.get('id')}"
    if camp_key in mapping:
        return normalize_product(mapping[camp_key])
    if row.get('adgroup'):
        by_group = classify_campaign(row['adgroup'])
        if by_group != 'أخرى':
            return by_group
    return classify_campaign(row.get('name', ''))


LINE_KEYWORDS = {
    'service': ['easy orders sign up', 'easy orders', 'ايزي اوردر', 'إيزي أوردر'],
    'vendor': ['فيندور', 'شبشب', 'سكتشر', 'بنطلون', 'جلد', 'slipper', 'تكس'],
}


def line_from_text(name):
    text = (name or '').lower()
    for line, keys in LINE_KEYWORDS.items():
        if any(k.lower() in text for k in keys):
            return line
    return 'kzamiza'


def line_for(platform, row, cfg):
    """خط الشغل: kzamiza (الأساسي) / vendor (أفلييت تاني) / service (مستبعد)."""
    lm = cfg.get('line_map') or {}
    ag_key = adgroup_key(platform, row)
    if ag_key and ag_key in lm:
        return lm[ag_key]
    camp_key = f"{platform}:{row.get('id')}"
    if camp_key in lm:
        return lm[camp_key]
    return line_from_text(f"{row.get('adgroup') or ''} {row.get('name') or ''}")


def rebuild(cfg):
    """يبني ad_spend.json من المصادر المفعّلة (يدوي + منصات)."""
    sources = enabled_sources(cfg)
    agg = {}

    def add(date, product, amount):
        product = normalize_product(product)
        agg.setdefault(date, {})
        agg[date][product] = round(agg[date].get(product, 0.0) + float(amount or 0), 2)

    if 'manual' in sources:
        for date, products in load_manual().items():
            for product, amount in (products or {}).items():
                add(date, product, amount)

    rows = load_rows()
    lines = {}
    for platform in PLATFORMS:
        if platform not in sources:
            continue
        for date, items in (rows.get(platform) or {}).items():
            for r in items:
                line = line_for(platform, r, cfg)
                product = row_product(platform, r, cfg)
                if line == 'kzamiza':
                    add(date, product, r['spend'])
                else:
                    bucket = lines.setdefault(line, {}).setdefault(date, {})
                    bucket[product] = round(bucket.get(product, 0.0) + float(r['spend'] or 0), 2)

    _write_json(AGG_FILE, agg)
    _write_json(LINES_FILE, lines)
    return agg


def load_lines():
    return _read_json(LINES_FILE, {})


def load_aggregate():
    return _read_json(AGG_FILE, {})


def _month_match(date_str, month):
    if not month or month == 'الكل':
        return True
    return (date_str or '').endswith(f"-{month}")


def _in_range(date_str, month, start=None, end=None):
    if start and end:
        try:
            d = datetime.strptime(date_str, '%d-%m-%Y').date()
            s = datetime.strptime(start, '%Y-%m-%d').date()
            e = datetime.strptime(end, '%Y-%m-%d').date()
            return s <= d <= e
        except ValueError:
            return False
    return _month_match(date_str, month)


def product_platform_spend(cfg, month='الكل', start=None, end=None):
    """مصروف كل منتج مقسوم على المنصات (خط كزاميزا فقط)."""
    out = {}
    for platform in PLATFORMS:
        for date, items in (load_rows().get(platform) or {}).items():
            if not _in_range(date, month, start, end):
                continue
            for r in items:
                if line_for(platform, r, cfg) != 'kzamiza':
                    continue
                product = row_product(platform, r, cfg)
                bucket = out.setdefault(product, {p: 0.0 for p in PLATFORMS})
                bucket[platform] = round(bucket.get(platform, 0.0) + float(r.get('spend') or 0), 2)
    return out


def funnel(cfg, month='الكل', start=None, end=None):
    """قمع تحويل اللاند بيدج لكل منتج (من أحداث Meta Pixel)."""
    buckets = {p: {
        'name': p, 'spend': 0.0, 'impressions': 0, 'clicks': 0,
        'lp_views': 0, 'atc': 0, 'checkouts': 0, 'purchases': 0,
        'purchase_value': 0.0,
    } for p in PRODUCTS}

    for platform in PLATFORMS:
        for date, items in (load_rows().get(platform) or {}).items():
            if not _in_range(date, month, start, end):
                continue
            for r in items:
                product = row_product(platform, r, cfg)
                bucket = buckets.setdefault(product, {
                    'name': product, 'spend': 0.0, 'impressions': 0, 'clicks': 0,
                    'lp_views': 0, 'atc': 0, 'checkouts': 0, 'purchases': 0,
                    'purchase_value': 0.0,
                })
                bucket['spend'] += float(r.get('spend') or 0)
                for field in ('impressions', 'clicks', 'lp_views', 'atc', 'checkouts', 'purchases'):
                    bucket[field] += int(r.get(field) or 0)
                bucket['purchase_value'] += float(r.get('purchase_value') or 0)

    def finalize(d):
        lp = d['lp_views']
        purchases = d['purchases']
        return {
            **d,
            'spend': round(d['spend'], 2),
            'purchase_value': round(d['purchase_value'], 2),
            'cvr': round(purchases / lp * 100, 2) if lp else 0.0,
            'atc_rate': round(d['atc'] / lp * 100, 2) if lp else 0.0,
            'checkout_rate': round(d['checkouts'] / lp * 100, 2) if lp else 0.0,
            'cost_per_lp': round(d['spend'] / lp, 2) if lp else 0.0,
            'cpa_pixel': round(d['spend'] / purchases, 2) if purchases else 0.0,
            'avg_order_value': round(d['purchase_value'] / purchases, 2) if purchases else 0.0,
            'ctr': round(d['clicks'] / d['impressions'] * 100, 2) if d['impressions'] else 0.0,
            'cpc': round(d['spend'] / d['clicks'], 2) if d['clicks'] else 0.0,
        }

    products = [finalize(buckets[p]) for p in PRODUCTS if p in buckets]
    totals = {k: sum(b[k] for b in products) for k in
              ('spend', 'impressions', 'clicks', 'lp_views', 'atc', 'checkouts', 'purchases')}
    totals['purchase_value'] = sum(b['purchase_value'] for b in products)
    totals['name'] = 'الإجمالي'
    return {
        'products': products,
        'totals': finalize(totals),
        'has_pixel_data': totals['lp_views'] > 0,
    }


def breakdown(cfg, month='الكل', level='campaign', start=None, end=None):
    """تفصيل المصروف والأداء على مستوى حملة / مجموعة إعلانية / إعلان."""
    groups = {}
    for platform in PLATFORMS:
        for date, items in (load_rows().get(platform) or {}).items():
            if not _in_range(date, month, start, end):
                continue
            for r in items:
                if level == 'adgroup' and not r.get('adgroup'):
                    continue
                if level == 'ad' and not r.get('ad_id'):
                    continue

                base = f"{platform}:{r['id']}"
                if level == 'campaign':
                    key = base
                    name = r['name']
                    parent = ''
                elif level == 'adgroup':
                    key = f"{base}:{r['adgroup']}"
                    name = r['adgroup']
                    parent = r['name']
                else:
                    key = f"{base}:{r.get('adgroup', '')}:{r.get('ad_id', '')}"
                    ad_type = r.get('ad_type') or ''
                    ad_id = r.get('ad_id') or ''
                    name = (ad_type + ' ') if ad_type else 'إعلان '
                    name = (name + f"#{ad_id}") if ad_id else (name or 'إعلان')
                    parent = f"{r['name']} / {r.get('adgroup', '')}"

                g = groups.get(key)
                if not g:
                    g = groups[key] = {
                        'key': key,
                        'platform': platform,
                        'name': name,
                        'parent': parent,
                        'product': row_product(platform, r, cfg),
                        'spend': 0.0,
                        'impressions': 0,
                        'clicks': 0,
                    }
                g['spend'] = round(g['spend'] + float(r.get('spend') or 0), 2)
                g['impressions'] += int(r.get('impressions') or 0)
                g['clicks'] += int(r.get('clicks') or 0)

    result = []
    for g in groups.values():
        g['ctr'] = round(g['clicks'] / g['impressions'] * 100, 2) if g['impressions'] else 0.0
        g['cpc'] = round(g['spend'] / g['clicks'], 2) if g['clicks'] else 0.0
        result.append(g)
    result.sort(key=lambda x: x['spend'], reverse=True)

    total = {
        'spend': round(sum(g['spend'] for g in result), 2),
        'impressions': sum(g['impressions'] for g in result),
        'clicks': sum(g['clicks'] for g in result),
        'count': len(result),
    }
    return {'level': level, 'rows': result, 'totals': total}


def save_config(cfg):
    _write_json(CONFIG_FILE, cfg)


def load_config():
    return _read_json(CONFIG_FILE, {})


def merge_config(cfg, patch):
    """دمج إعدادات جديدة مع تجاهل القيم المقنّعة/الفارغة."""
    def merge(dst, src):
        for k, v in (src or {}).items():
            if isinstance(v, dict) and isinstance(dst.get(k), dict):
                merge(dst[k], v)
            elif isinstance(v, str):
                v = v.strip()
                if v and not v.startswith('•'):
                    dst[k] = v
            elif isinstance(v, (int, float, bool, list)):
                dst[k] = v
    merge(cfg, patch)
    return cfg


# --------------------------------------------------------------------------
# مزامنة
# --------------------------------------------------------------------------

def meta_accounts(cfg):
    """كل حسابات ميتا المربوطة (مع ترحيل الحساب القديم المفرد)."""
    accounts = cfg.get('meta_accounts')
    if isinstance(accounts, list) and accounts:
        out = []
        for a in accounts:
            account_id = (a.get('ad_account_id') or '').strip()
            token = (a.get('access_token') or '').strip()
            if not account_id or not token:
                continue
            if not account_id.startswith('act_'):
                account_id = 'act_' + account_id
            out.append({
                'ad_account_id': account_id,
                'access_token': token,
                'api_version': a.get('api_version') or 'v21.0',
                'label': a.get('label') or '',
            })
        return out

    meta = cfg.get('meta') or {}
    account_id = (meta.get('ad_account_id') or '').strip()
    token = (meta.get('access_token') or '').strip()
    if account_id and token:
        if not account_id.startswith('act_'):
            account_id = 'act_' + account_id
        return [{
            'ad_account_id': account_id,
            'access_token': token,
            'api_version': meta.get('api_version') or 'v21.0',
            'label': '',
        }]
    return []


def sync(platform, cfg, month=None, account=None):
    if platform not in PLATFORMS:
        raise AdsError("منصة غير مدعومة")

    since, until = month_to_range(month) if month and month != 'الكل' else default_range()

    if platform == 'meta':
        accounts = meta_accounts(cfg)
        if account:
            accounts = [a for a in accounts if a['ad_account_id'] == account]
        if not accounts:
            raise AdsError("مفيش حساب ميتا مربوط — ضيف حساب وتوكنه الأول.")

        rows = []
        state = load_state()
        state.setdefault('meta_accounts', {})
        for acc in accounts:
            client = MetaAds(acc['ad_account_id'], acc['access_token'],
                             acc.get('api_version') or 'v21.0')
            acc_rows = client.daily_campaign_spend(since, until)
            for r in acc_rows:
                r['account'] = acc['ad_account_id']
            rows.extend(acc_rows)
            state['meta_accounts'][acc['ad_account_id']] = {
                'synced_at': datetime.now().strftime('%Y-%m-%d %H:%M'),
                'since': since,
                'until': until,
                'rows': len(acc_rows),
                'label': acc.get('label') or '',
            }
    else:
        client = GoogleAds(cfg.get('google_ads'))
        rows = client.daily_campaign_spend(since, until)
        account = client.customer_id
        state = load_state()

    store_rows(platform, rows, cfg)

    state[platform] = {
        'synced_at': datetime.now().strftime('%Y-%m-%d %H:%M'),
        'since': since,
        'until': until,
        'rows': len(rows),
        'account': account,
    }
    save_state(state)

    # أول مزامنة ناجحة للمنصة: فعّلها تلقائياً بدل المصدر اليدوي
    _activate_source(cfg, platform)

    rebuild(cfg)
    return {
        'platform': platform,
        'rows': len(rows),
        'total': round(sum(r['spend'] for r in rows), 2),
        'since': since,
        'until': until,
        'accounts': len(accounts) if platform == 'meta' else 1,
    }


def test(platform, cfg, account=None):
    if platform == 'meta':
        accounts = meta_accounts(cfg)
        if account:
            accounts = [a for a in accounts if a['ad_account_id'] == account]
        if not accounts:
            raise AdsError("مفيش حساب ميتا مربوط")
        acc = accounts[0]
        client = MetaAds(acc['ad_account_id'], acc['access_token'],
                         acc.get('api_version') or 'v21.0')
        data = client.account()
        return {'account': data.get('name'), 'currency': data.get('currency'),
                'timezone': data.get('timezone_name'), 'status': data.get('account_status'),
                'account_id': acc['ad_account_id']}
    if platform == 'google':
        client = GoogleAds(cfg.get('google_ads'))
        results = client.search("SELECT customer.id, customer.descriptive_name FROM customer LIMIT 1")
        data = results[0].get('customer', {}) if results else {}
        return {'account': data.get('descriptiveName') or client.customer_id,
                'customer_id': data.get('id') or client.customer_id}
    raise AdsError("منصة غير مدعومة")


def _activate_source(cfg, platform):
    """أول استخدام ناجح للمنصة: فعّلها، مع الحفاظ على أي مصدر تلقائي مفعّل."""
    sources = enabled_sources(cfg)
    if platform in sources:
        return
    auto = [p for p in sources if p in ('meta', 'google')]
    auto.append(platform)
    cfg['spend_sources_enabled'] = auto
    save_config(cfg)


# --------------------------------------------------------------------------
# استيراد تقارير CSV من Ads Manager (بدون توكن)
# --------------------------------------------------------------------------

_NAME_KEYS = ['campaignname', 'campaign', 'اسمالحملة', 'الحملة', 'اسمحمله', 'اسمحملة']
_ID_KEYS = ['campaignid', 'معرفالحملة', 'رقمالحملة']
_SPEND_KEYS = ['amountspent', 'spend', 'cost', 'المبلغالذيتمإنفاقه', 'المبلغالمنفق',
               'المصروف', 'التكلفة', 'إنفاق', 'المنفق', 'المبلغالمصروف']
_DATE_KEYS = ['day', 'date', 'reportingstarts', 'اليوم', 'التاريخ', 'بدايةالتقارير', 'تاريخالبدء']
_ADGROUP_KEYS = ['adgroupname', 'adgroup', 'المجموعةالإعلانية', 'المجموعة']
_AD_ID_KEYS = ['adid', 'معرفالإعلان']
_AD_TYPE_KEYS = ['adtype', 'نوعالإعلان']
_IMPR_KEYS = ['impressions', 'الظهور']
_CLICK_KEYS = ['clicks', 'النقرات']

_TOTAL_MARKERS = ('الإجمالي', 'اجمالي', 'total', 'المجموع', 'results', 'النتائج')


def _norm_header(value):
    s = (value or '').replace('\ufeff', '').strip().lower()
    s = re.sub(r'\s+', '', s)
    s = re.sub(r'[^\w\u0600-\u06FF]+', '', s)
    return s


def _pick_column(headers, keys, exclude=()):
    candidates = [(h, _norm_header(h)) for h in headers if h and h not in exclude]
    for key in keys:
        for h, n in candidates:
            if n == key:
                return h
    for key in keys:
        for h, n in candidates:
            if n.startswith(key):
                return h
    for key in keys:
        for h, n in candidates:
            if key in n:
                return h
    return None


def _decode_report(content):
    """يدعم UTF-8 و UTF-16 (وبعض ملفات Excel العربية)."""
    if isinstance(content, bytes):
        raw = content
    else:
        raw = content.encode('utf-8', 'ignore')

    if raw[:2] in (b'\xff\xfe', b'\xfe\xff') or raw.count(b'\x00') > 10:
        for enc in ('utf-16', 'utf-16-le', 'utf-16-be'):
            try:
                return raw.decode(enc)
            except Exception:
                continue
    for enc in ('utf-8-sig', 'utf-8', 'cp1256', 'latin-1'):
        try:
            return raw.decode(enc)
        except Exception:
            continue
    return raw.decode('utf-8', 'replace')


_AR_DIGITS = str.maketrans('٠١٢٣٤٥٦٧٨٩۰۱۲۳۴۵۶۷۸۹', '01234567890123456789')


def _parse_money(value):
    s = str(value or '')
    s = s.translate(_AR_DIGITS)
    s = s.replace('\u200f', '').replace('\u200e', '').replace('\xa0', ' ')
    # الفاصلة العشرية/فاصلة الآلاف العربية
    s = s.replace('\u066b', '.').replace('\u066c', ',')
    s = re.sub(r'[^\d.,\-]', '', s)
    if not s or s in ('-', '.', ','):
        return 0.0
    if ',' in s and '.' in s:
        if s.rfind(',') > s.rfind('.'):
            s = s.replace('.', '').replace(',', '.')
        else:
            s = s.replace(',', '')
    elif ',' in s:
        parts = s.split(',')
        s = s.replace(',', '.') if len(parts) == 2 and len(parts[1]) <= 2 else s.replace(',', '')
    try:
        return float(s)
    except ValueError:
        return 0.0


def _parse_report_date(value):
    s = str(value or '').strip()
    s = re.sub(r'\s+', ' ', s)
    for fmt in ('%Y-%m-%d', '%d/%m/%Y', '%d-%m-%Y', '%m/%d/%Y', '%Y/%m/%d',
                '%d.%m.%Y', '%Y-%m-%d %H:%M:%S', '%d/%m/%Y %H:%M:%S'):
        try:
            return datetime.strptime(s, fmt).strftime('%d-%m-%Y')
        except ValueError:
            continue
    return None


def parse_report(content):
    """يحوّل تقرير Ads Manager (CSV/TSV) إلى صفوف {date,id,name,spend}."""
    text = _decode_report(content)
    lines = text.splitlines()
    if not lines:
        raise AdsError("الملف فاضي")

    sample = '\n'.join(lines[:5])
    if sample.count('\t') > sample.count(',') and sample.count('\t') > sample.count(';'):
        delimiter = '\t'
    elif sample.count(';') > sample.count(','):
        delimiter = ';'
    else:
        delimiter = ','

    table = [row for row in csv.reader(io.StringIO(text), delimiter=delimiter)]
    if not table:
        raise AdsError("مش قادر أقرأ الملف")

    best = None
    for idx, row in enumerate(table[:15]):
        headers = [h.strip() for h in row]
        spend_col = _pick_column(headers, _SPEND_KEYS)
        id_col = _pick_column(headers, _ID_KEYS)
        name_col = _pick_column(headers, _NAME_KEYS, exclude={id_col} if id_col else ())
        date_col = _pick_column(headers, _DATE_KEYS)
        score = (2 if spend_col else 0) + (2 if (name_col or id_col) else 0) + (1 if date_col else 0)
        if spend_col and (name_col or id_col) and (best is None or score > best[0]):
            best = (score, idx, headers, spend_col, name_col, id_col, date_col)

    if not best:
        found = ', '.join([h for h in table[0][:8] if h])[:200]
        raise AdsError(
            "مش لاقي أعمدة الحملة والمصروف في الملف. لازم التقرير يحتوي على عمود "
            f"اسم الحملة وعمود المبلغ المنفق. الأعمدة الموجودة: {found}")

    _, header_idx, headers, spend_col, name_col, id_col, date_col = best
    if not date_col:
        raise AdsError(
            "الملف فيه اسم الحملة والتكلفة، لكن ناقص عمود اليوم (Day) — ومن غيره "
            "مش هنقدر نوزّع المصروف على الشهور. في Google Ads: من فوق الجدول "
            "اختار «تقسيم / Segment» ← «اليوم / Day» وبعدها نزّل CSV تاني.")

    positions = {name: i for i, name in enumerate(headers) if name}
    used = {c for c in (name_col, id_col, spend_col, date_col) if c}
    adgroup_col = _pick_column(headers, _ADGROUP_KEYS, exclude=used)
    used.add(adgroup_col)
    ad_id_col = _pick_column(headers, _AD_ID_KEYS, exclude=used)
    used.add(ad_id_col)
    ad_type_col = _pick_column(headers, _AD_TYPE_KEYS, exclude=used)
    used.add(ad_type_col)
    impr_col = _pick_column(headers, _IMPR_KEYS, exclude=used)
    used.add(impr_col)
    clicks_col = _pick_column(headers, _CLICK_KEYS, exclude=used)

    rows = []
    for row in table[header_idx + 1:]:
        if not any(cell.strip() for cell in row):
            continue
        def cell(col):
            i = positions.get(col)
            return row[i].strip() if i is not None and i < len(row) else ''

        name = cell(name_col) if name_col else ''
        cid = cell(id_col) if id_col else ''
        if name and any(m in name.lower() for m in _TOTAL_MARKERS):
            continue
        date = _parse_report_date(cell(date_col))
        if not date:
            continue
        spend = _parse_money(cell(spend_col))
        if spend <= 0:
            continue

        rec = {
            'date': date,
            'id': cid or name,
            'name': name or cid or '(حملة بدون اسم)',
            'spend': round(spend, 2),
        }
        if adgroup_col:
            rec['adgroup'] = cell(adgroup_col) or '(بدون مجموعة)'
        if ad_id_col:
            rec['ad_id'] = cell(ad_id_col)
        if ad_type_col:
            rec['ad_type'] = cell(ad_type_col)
        if impr_col:
            rec['impressions'] = _to_int(_parse_money(cell(impr_col)))
        if clicks_col:
            rec['clicks'] = _to_int(_parse_money(cell(clicks_col)))
        rows.append(rec)

    if not rows:
        raise AdsError("مفيش صفوف فيها مصروف وتاريخ صالح في الملف")

    # تجميع التكرار لنفس (اليوم + الحملة + المجموعة + الإعلان)
    merged = {}
    for r in rows:
        key = (r['date'], r['id'], r.get('adgroup', ''), r.get('ad_id', ''))
        if key in merged:
            merged[key]['spend'] = round(merged[key]['spend'] + r['spend'], 2)
            merged[key]['impressions'] = merged[key].get('impressions', 0) + r.get('impressions', 0)
            merged[key]['clicks'] = merged[key].get('clicks', 0) + r.get('clicks', 0)
        else:
            merged[key] = dict(r)
    rows = list(merged.values())
    rows.sort(key=lambda r: (datetime.strptime(r['date'], '%d-%m-%Y'), r['name']))
    return rows


def import_report(platform, content, cfg, filename=''):
    if platform not in PLATFORMS:
        raise AdsError("منصة غير مدعومة")
    rows = parse_report(content)
    store_rows(platform, rows, cfg)
    _activate_source(cfg, platform)

    ordered = sorted(rows, key=lambda r: datetime.strptime(r['date'], '%d-%m-%Y'))
    state = load_state()
    state[platform] = {
        'synced_at': datetime.now().strftime('%Y-%m-%d %H:%M'),
        'since': ordered[0]['date'],
        'until': ordered[-1]['date'],
        'rows': len(rows),
        'account': None,
        'source': 'import',
        'file': filename,
    }
    save_state(state)
    rebuild(cfg)
    return {
        'platform': platform,
        'rows': len(rows),
        'total': round(sum(r['spend'] for r in rows), 2),
        'since': state[platform]['since'],
        'until': state[platform]['until'],
    }


def sheet_csv_url(url):
    """يحوّل رابط الشيت إلى رابط تصدير CSV (مع دعم gid للتاب المطلوب)."""
    m = re.search(r'/spreadsheets/d/([a-zA-Z0-9-_]+)', url or '')
    if not m:
        raise AdsError("رابط شيت جوجل غير صالح — انسخ رابط الشيت كامل.")
    sheet_id = m.group(1)
    gid = re.search(r'[#&?]gid=(\d+)', url or '')
    if gid:
        return (f"https://docs.google.com/spreadsheets/d/{sheet_id}/export?format=csv"
                f"&gid={gid.group(1)}")
    tab = re.search(r'[?&]sheet=([^&#]+)', url or '')
    if tab:
        return (f"https://docs.google.com/spreadsheets/d/{sheet_id}/gviz/tq?tqx=out:csv"
                f"&sheet={urllib.parse.quote(tab.group(1))}")
    return f"https://docs.google.com/spreadsheets/d/{sheet_id}/export?format=csv"


def fetch_sheet_csv(url):
    headers = {"User-Agent": "Mozilla/5.0"}
    req = urllib.request.Request(sheet_csv_url(url), headers=headers)
    try:
        with urllib.request.urlopen(req, context=_SSL, timeout=25) as resp:
            return resp.read()
    except urllib.error.HTTPError as e:
        if e.code in (401, 403):
            raise AdsError("الشيت مش متاح — شاركه كـ «أي شخص عنده الرابط» أو انشره على الويب.")
        raise AdsError(f"تعذر تنزيل الشيت ({e.code})")
    except Exception as e:
        raise AdsError(f"تعذر الاتصال بالشيت: {e}")


def sync_sheet(url, cfg, platform='google'):
    """يقرأ شيت جوجل (Day / Campaign / Cost) ويحفظه كمصروف للمنصة."""
    blob = fetch_sheet_csv(url)
    rows = parse_report(blob)
    store_rows(platform, rows, cfg)
    _activate_source(cfg, platform)

    state = load_state()
    state.pop('sheet_error', None)
    ordered = sorted(rows, key=lambda r: datetime.strptime(r['date'], '%d-%m-%Y'))
    state['sheet'] = {
        'synced_at': datetime.now().strftime('%Y-%m-%d %H:%M'),
        'rows': len(rows),
        'since': ordered[0]['date'],
        'until': ordered[-1]['date'],
        'platform': platform,
        'file': 'google_sheet',
    }
    save_state(state)
    rebuild(cfg)
    return {
        'rows': len(rows),
        'total': round(sum(r['spend'] for r in rows), 2),
        'since': state['sheet']['since'],
        'until': state['sheet']['until'],
    }


def migrate_legacy_spend():
    """ينقل ad_spend.json القديم إلى manual_spend.json مرة واحدة."""
    if os.path.exists(MANUAL_FILE) or not os.path.exists(AGG_FILE):
        return False
    legacy = _read_json(AGG_FILE, {})
    if not legacy:
        return False
    manual = {}
    for date, products in legacy.items():
        for product, amount in (products or {}).items():
            manual.setdefault(date, {})[normalize_product(product)] = amount
    _write_json(MANUAL_FILE, manual)
    return True


def ensure_defaults(cfg):
    cfg.setdefault('spend_sources_enabled', ['manual'])
    cfg.setdefault('campaign_map', {})
    cfg.setdefault('meta', {
        'access_token': '', 'ad_account_id': '', 'api_version': 'v21.0',
    })
    cfg.setdefault('google_ads', {
        'developer_token': '', 'client_id': '', 'client_secret': '',
        'refresh_token': '', 'customer_id': '', 'login_customer_id': '',
        'api_version': 'v21',
    })
    return cfg
