File size: 58,721 Bytes
943ce83
 
 
 
 
33b2436
 
693b2c6
 
 
 
 
943ce83
 
 
 
 
 
 
2c40ff6
943ce83
 
2c40ff6
 
943ce83
 
 
33b2436
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4acad52
943ce83
 
 
4acad52
943ce83
 
 
 
 
 
8299dbb
943ce83
 
 
 
33b2436
 
 
 
 
 
943ce83
33b2436
 
 
 
 
 
 
 
 
 
 
 
 
943ce83
 
 
 
 
 
4acad52
 
 
33b2436
 
 
 
 
 
8299dbb
 
33b2436
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8299dbb
33b2436
 
 
 
 
 
 
 
 
 
 
 
4acad52
 
9f1960c
4acad52
 
 
9f1960c
4acad52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
943ce83
 
 
 
4acad52
943ce83
4acad52
943ce83
 
 
 
 
cd18006
 
943ce83
4acad52
943ce83
 
cd18006
943ce83
 
 
4acad52
 
 
 
 
 
 
 
943ce83
4acad52
 
 
 
 
8299dbb
943ce83
 
4acad52
943ce83
4acad52
 
 
8299dbb
943ce83
 
4acad52
 
 
 
 
943ce83
4acad52
 
 
 
943ce83
4acad52
943ce83
4acad52
 
 
 
8299dbb
 
 
 
cd18006
8299dbb
 
 
 
 
 
943ce83
 
 
 
 
 
4acad52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cd18006
4acad52
 
 
 
 
cd18006
4acad52
 
 
 
 
 
cd18006
4acad52
 
 
cd18006
4acad52
 
 
cd18006
 
4acad52
cd18006
 
 
 
 
 
 
 
33b2436
cd18006
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4acad52
cd18006
 
 
 
 
 
4acad52
cd18006
4acad52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33b2436
 
943ce83
4acad52
33b2436
 
4acad52
33b2436
 
 
 
 
 
 
943ce83
4acad52
33b2436
4acad52
 
 
 
 
 
 
33b2436
4acad52
33b2436
 
 
 
 
 
 
4acad52
 
 
8299dbb
 
cd18006
8299dbb
 
 
 
 
cd18006
 
8299dbb
 
 
cd18006
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8299dbb
4acad52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cd18006
 
 
 
 
 
 
 
4acad52
 
 
 
 
 
 
 
 
cd18006
 
 
 
4acad52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8299dbb
4acad52
 
 
 
 
 
33b2436
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8299dbb
33b2436
 
e35d893
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
943ce83
 
e35d893
943ce83
 
5899083
d7d5215
943ce83
5899083
d7d5215
943ce83
 
 
d7d5215
 
 
 
 
 
4acad52
d7d5215
 
4acad52
d7d5215
 
943ce83
d7d5215
943ce83
d7d5215
959ca57
 
943ce83
 
 
4acad52
e35d893
943ce83
 
d7d5215
 
 
 
 
 
959ca57
 
 
 
 
9b683d3
959ca57
 
9b683d3
 
7715f54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7641115
 
 
7715f54
9b683d3
7715f54
 
 
 
 
9b683d3
 
7715f54
 
 
9b683d3
 
7715f54
 
 
9b683d3
 
 
 
 
 
959ca57
 
 
 
d7d5215
 
 
 
 
 
 
 
 
960f2f1
d7d5215
 
 
 
960f2f1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
import gradio as gr
import requests
import os
import re
import math
import time
import datetime
import spaces

@spaces.GPU
def dummy_gpu():
    pass

MODEL_NAME = "gemini-flash-lite-latest"
GEMINI_URL = f"https://generativelanguage.googleapis.com/v1beta/models/{MODEL_NAME}:generateContent"

def get_api_keys():
    keys = []
    for i in range(1, 4):
        k = os.environ.get(f"GEMINI_API_KEY{i}", "").strip()
        if k:
            keys.append(k)
    fallback = os.environ.get("GEMINI_API_KEY", "").strip()
    if fallback and fallback not in keys:
        keys.append(fallback)
    return keys

def _fetch_with_retry(key: str, payload: dict, max_retries: int = 3) -> requests.Response:
    """
    يحاول إرسال طلب لمفتاح واحد حتى max_retries مرات.
    - عند 503 أو 'high demand'/'overloaded': ينتظر ويعيد المحاولة (exponential backoff).
    - عند quota (429/RESOURCE_EXHAUSTED): يرفع QuotaError فوراً للانتقال للمفتاح التالي.
    - عند أي خطأ آخر غير قابل للتكرار: يرفعه فوراً.
    """
    headers = {"x-goog-api-key": key, "Content-Type": "application/json"}
    backoff = 3  # ثواني
    for attempt in range(1, max_retries + 1):
        try:
            resp = requests.post(GEMINI_URL, headers=headers, json=payload, timeout=120)
        except requests.RequestException as e:
            if attempt == max_retries:
                raise
            print(f"[generate] ⚠️ خطأ شبكة (محاولة {attempt}/{max_retries}): {e}، انتظار {backoff}s...")
            time.sleep(backoff)
            backoff *= 2
            continue

        if not resp.ok:
            try:
                err_data = resp.json()
            except Exception:
                err_data = {}
            status_str = err_data.get("error", {}).get("status", "")
            msg = err_data.get("error", {}).get("message", "").lower()

            is_quota = (
                status_str == "RESOURCE_EXHAUSTED"
                or resp.status_code == 429
                or "quota" in msg
            )
            if is_quota:
                raise _QuotaError()

            is_high_demand = (
                resp.status_code == 503
                or "high demand" in msg
                or "overloaded" in msg
            )
            if is_high_demand:
                if attempt == max_retries:
                    raise Exception(
                        "الخادم مشغول حالياً بعد عدة محاولات. يرجى المحاولة مرة أخرى بعد قليل."
                    )
                print(f"[generate] 🔄 ضغط عالٍ على الخادم (محاولة {attempt}/{max_retries})، انتظار {backoff}s...")
                time.sleep(backoff)
                backoff *= 2
                continue

            # خطأ غير قابل للتكرار
            err_msg = err_data.get("error", {}).get("message", f"HTTP {resp.status_code}")
            raise Exception(err_msg)

        return resp  # نجاح

    raise Exception("الخادم مشغول حالياً بعد عدة محاولات. يرجى المحاولة مرة أخرى بعد قليل.")


class _QuotaError(Exception):
    """رمز داخلي: المفتاح الحالي استنفد الحصة — انتقل للتالي."""
    pass


def call_gemini(prompt: str, keys: list, temperature: float = 0.7, use_thinking: bool = True, max_tokens: int = 15000):
    payload = {
        "contents": [{"parts": [{"text": prompt}]}],
        "generationConfig": {
            "maxOutputTokens": max_tokens,
            "temperature": temperature,
        }
    }
    if use_thinking:
        payload["generationConfig"]["thinkingConfig"] = {"thinkingLevel": "low"}

    print(f"[generate] 🚀 إرسال طلب لـ Gemini | use_thinking={use_thinking} | tokens={max_tokens}")
    last_error = "لا يوجد مفتاح API متاح."

    for i, key in enumerate(keys):
        try:
            resp = _fetch_with_retry(key, payload)
        except _QuotaError:
            next_i = i + 1
            if next_i < len(keys):
                print(f"[generate] ⚠️ المفتاح رقم {i+1} استنفد الحصة، الانتقال للمفتاح رقم {next_i+1}...")
            last_error = f"المفتاح رقم {i+1} استنفد الحصة."
            continue
        except Exception as e:
            # خطأ شبكة أو خادم مشغول بعد كل المحاولات — يوقف المحاولات الكاملة
            raise Exception(str(e))

        data = resp.json()
        parts = data.get("candidates", [{}])[0].get("content", {}).get("parts", [])
        text = "\n".join(p.get("text", "") for p in parts if not p.get("thought") and p.get("text"))

        if data.get("candidates", [{}])[0].get("finishReason") == "MAX_TOKENS" and use_thinking:
            print("[generate] تحذير: MAX_TOKENS، إعادة المحاولة بدون thinking...")
            return call_gemini(prompt, keys, temperature, use_thinking=False, max_tokens=max_tokens)

        return text

    raise Exception(
        f"تم استهلاك الحد اليومي لجميع مفاتيح API المتاحة ({len(keys)} مفاتيح). "
        f"يُرجى المحاولة غداً بعد تجديد الحد."
    )

def split_keywords(raw: str):
    return [k.strip() for k in re.split(r'،|,|\s+-\s+', raw) if k.strip()]


def log_seo_report(
    title, primary_keyword, target_wc, final_wc, final_wc_diff,
    final_kw_count, dyn_min, dyn_max, final_density, final_in_100,
    sec_keys, dyn_min_sec, article, warnings,
    was_wc_corrected, wc_before_correction, stable_wc,
    was_kw_added, was_kw_reduced, conclusion_corrected,
    was_sec_kw_added=False
):
    """طباعة تقرير منظم في console لمراجعته في HuggingFace Logs لاحقاً."""
    timestamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
    sep = "=" * 62

    wc_status  = "✅" if abs(final_wc_diff) <= 30 else "❌"
    if dyn_min <= final_kw_count <= dyn_max:
        kw_status = "✅"
    elif final_kw_count < dyn_min:
        kw_status = "❌"
    else:
        kw_status = "⚠️"

    sec_lines = []
    for kw in sec_keys:
        c = len(re.findall(re.escape(kw), article, re.IGNORECASE))
        s = "✅" if c >= dyn_min_sec else "❌"
        sec_lines.append(f"    {kw}: {c} مرات {s}")

    lines = [
        f"\n{sep}",
        f"📋 SEO ARTICLE LOG  |  {timestamp}",
        sep,
        f"📌 العنوان        : {title[:65]}{'...' if len(title) > 65 else ''}",
        f"🔑 الكلمة الأساسية: {primary_keyword}",
        sep,
        f"📏 عدد الكلمات:",
        f"    المستهدف       : {target_wc}",
        f"    الفعلي النهائي : {final_wc} {wc_status}",
        f"    الفرق          : {'+' if final_wc_diff > 0 else ''}{final_wc_diff}",
        f"    تصحيح WC       : {'🔄 تم (' + str(wc_before_correction) + '→' + str(stable_wc) + ')' if was_wc_corrected else '✅ لم يحتج'}",
        sep,
        f"🎯 الكلمة المفتاحية:",
        f"    التكرار النهائي: {final_kw_count} (المطلوب: {dyn_min}{dyn_max}) {kw_status}",
        f"    الكثافة        : {final_density}%",
        f"    ظهور أول 100   : {'نعم ✅' if final_in_100 else 'لا ❌'}",
        f"    تصحيح إضافة   : {'🔄 تم' if was_kw_added else '✅ لم يحتج'}",
        f"    تصحيح حذف     : {'✂️ تم' if was_kw_reduced else '✅ لم يحتج'}",
        f"    تصحيح خلاصة   : {'🔄 تم' if conclusion_corrected else '✅ لم يحتج'}",
    ]
    if sec_lines:
        lines.append(sep)
        lines.append(f"🔑 الكلمات الثانوية (حد أدنى: {dyn_min_sec} لكل كلمة):")
        lines.append(f"    تصحيح الكلمات الثانوية: {'🔄 تم' if was_sec_kw_added else '✅ لم يحتج'}")
        lines.extend(sec_lines)
    lines.append(sep)
    if warnings:
        lines.append("⚠️  تحذيرات:")
        for ln in warnings.strip().split("\n"):
            if ln.strip():
                lines.append(f"    {ln.strip()}")
    else:
        lines.append("✅  لا توجد تحذيرات — المقال اجتاز جميع الفحوصات")
    lines.append(f"{sep}\n")
    print("\n".join(lines))

def generate_article(title, outline, primary_keyword, secondary_keywords, target_word_count, is_rewrite, rewrite_notes):
    if not title or not outline or not primary_keyword:
        return "❌ يرجى ملء جميع الحقول المطلوبة (العنوان، الـ Outline، الكلمة المفتاحية الأساسية).", ""

    keys = get_api_keys()
    if not keys:
        return "❌ لم يتم ضبط أي مفتاح Gemini API. يرجى إضافة GEMINI_API_KEY1 في إعدادات الـ Space.", ""

    # 1. Target word count
    try:
        target_wc = int(target_word_count)
        if target_wc < 800 or target_wc > 1500:
            target_wc = 900
    except (ValueError, TypeError):
        target_wc = 900

    # 2. Dynamic repeat calculation
    min_repeats = math.ceil(target_wc * 0.01)
    max_repeats = math.floor(target_wc * 0.015)
    min_secondary_repeats = max(3, round((target_wc / 900.0) * 3))

    sec_keys = split_keywords(secondary_keywords) if secondary_keywords else []
    secondary_list = "، ".join(sec_keys) if sec_keys else "لا يوجد"

    if target_word_count and 800 <= target_wc <= 1500:
        word_count_instruction = f"يجب أن يكون عدد كلمات المقال {target_wc} كلمة بالضبط (بهامش تفاوت لا يتجاوز ±30 كلمة). هذا رقم دقيق حدده المستخدم، التزم به بدقة."
    else:
        word_count_instruction = "يجب أن يتراوح عدد كلمات المقال بين 800 و1000 كلمة. هذا الشرط إلزامي بدون استثناء بغض النظر عن طبيعة الموضوع أو بساطته الظاهرية. حتى لو بدا الموضوع يمكن تغطيته بإيجاز، يجب عليك التوسع بأمثلة إضافية، تفاصيل عملية أكثر، أو شرح أعمق لكل نقطة في الـ Outline للوصول إلى العدد المطلوب دون حشو أو تكرار للمعنى نفسه بصياغات مختلفة."

    prompt = f"""أنت كاتب محتوى SEO محترف. مهمتك كتابة مقال متكامل وعالي الجودة بناءً على المعطيات التالية:

**عنوان المقال:** {title}

**الكلمة المفتاحية الأساسية:** {primary_keyword}

**الكلمات المفتاحية الثانوية:** {secondary_list}

**الـ Outline (العناوين الفرعية):**
{outline}

## ⚠️ قاعدة العناوين والـ Title (أولوية قصوى — لا استثناء):
العنوان الرئيسي للمقال (H1) يجب أن يكون مطابقاً تماماً لعنوان المقال المُدخل من المستخدم، دون أي إضافة أو توسيع أو صياغة فرعية، حتى لو بدت الإضافة تحسينية من الناحية التسويقية.
يجب استخدام العناوين الفرعية (H2) التالية بالضبط كما وردت، دون أي تعديل أو إضافة أو حذف أو إعادة صياغة:

{outline}

لا تُضف كلمات إلى العنوان الرئيسي أو العناوين الفرعية، ولا تُغيّر ترتيب الكلمات فيها، ولا تدمج كلمة مفتاحية داخلها إذا لم تكن موجودة أصلاً. استخدم العنوان حرفياً كما هو.

## تعليمات الكتابة:
- اكتب المقال كاملاً باللغة العربية الفصحى السلسة.
- {word_count_instruction}
- بعد المقدمة مباشرة وقبل أول عنوان فرعي (H2)، أضف فقرة بعنوان 'نظرة سريعة 👀' تتكون من سطرين يشرحان أهمية الموضوع باختصار، يليهما قائمة نقطية (bullet points) تلخص أهم النقاط التي سيتناولها المقال.

## هيكلة الأقسام (مرنة حسب نوع المحتوى):
لكل قسم فرعي (H2) في المقال، اتبع هذا الهيكل العام:
- فقرة تمهيدية قصيرة (2-3 جمل) تشرح أهمية هذه النقطة
- محتوى القسم الرئيسي
- جملة أو جملتين ختاميتين تلخصان الفقرة وتربطانها بالقسم التالي

إذا كان القسم الفرعي (H2) يحتوي على أكثر من فكرة رئيسية منفصلة، قسّمه إلى H3. لا تُقسّم القسم إلى H3 إذا كانت الفكرة الأساسية فيه واحدة ومترابطة.

بالنسبة لمحتوى القسم الرئيسي، اختر الصيغة الأنسب:
- استخدم نقاطاً (bullet points) للخطوات، المميزات، القوائم، والنصائح المنفصلة.
- استخدم فقرات متصلة (نثر) للشرح والسياق والمقارنات.
- لكل قسم فرعي (H2)، أضف مثالاً عملياً واقعياً أو سيناريو قصيراً يوضح الفكرة المطروحة (مثال: موقف يومي، حالة استخدام شائعة)، بدلاً من الاكتفاء بالشرح النظري فقط. هذا يزيد عمق المحتوى وقيمته الحقيقية للقارئ، وليس حشواً.

## الكتابة بأسلوب بشري طبيعي:
- تجنب تماماً: 'بالإضافة إلى ذلك'، 'علاوة على ذلك'، 'فضلاً عن ذلك'، 'في الختام'، 'يعد من أهم'، 'لا يخفى على أحد'، 'من الجدير بالذكر'.
- اربط الجمل بروابط طبيعية (لذلك، حيث، كما، بينما، غير أن، فإن، إذ).
- استخدم صيغة المبني للمعلوم (Active Voice).
- خاطب القارئ مباشرة باستخدام 'أنت'.
- ممنوع استخدام الكلمة المفتاحية الأساسية كجملة ختامية في نهاية أكثر من قسم واحد متتالٍ.
- اكتب بأسلوب بشري طبيعي وعفوي في اختيار الكلمات وتدفق الجمل، لكن هذا لا يعني كتابة جمل قصيرة أو مختصرة. يمكن للجملة أن تكون متوسطة إلى طويلة (15-25 كلمة) وتظل طبيعية غير مصطنعة، طالما أنها مترابطة منطقياً وتحمل معنى حقيقياً. لا تقصر أو تختصر أي فقرة لمجرد تبسيط الأسلوب - كل فقرة يجب أن تحتوي على 4-6 جمل على الأقل لتغطي الفكرة بعمق كافٍ.

## قواعد السيو:
اكتب المقال مع الالتزام الصارم بقواعد السيو التالية:
1. يجب أن تظهر الكلمة المفتاحية الأساسية '{primary_keyword}' في أول 100 كلمة من المقال.
2. ⚠️ (أولوية قصوى): يجب أن تتكرر الكلمة المفتاحية الأساسية '{primary_keyword}' حرفياً عدد {min_repeats} إلى {max_repeats} مرة بالضبط في المقال (وليس نسبة مئوية - عدّ التكرارات فعلياً أثناء الكتابة). هذا رقم دقيق وليس تقريبياً، التزم به بدقة.
مهم جداً: يجب أن يبقى كل استخدام للكلمة المفتاحية طبيعياً وسلساً ضمن سياق الجملة ويخدم القارئ فعلياً. ممنوع منعاً باتاً حشو الكلمة المفتاحية بشكل مصطنع. الأولوية القصوى دائماً هي أن يشعر القارئ أن كل جملة تقدم له قيمة حقيقية.
عند التوسع في المحتوى، يجب الحفاظ على تكرار الكلمة المفتاحية في الأجزاء الموسّعة أيضاً. إذا كانت الكلمة المفتاحية تحتوي على أكثر من كلمة، لا تستبدلها بصيغة مختصرة أو مرادف، بل استخدم العبارة الكاملة كما وردت حرفياً.
3. وزّع تكرار الكلمة المفتاحية على طول المقال.
4. يجب أن تتكرر كل كلمة مفتاحية ثانوية {min_secondary_repeats} مرات بشكل طبيعي في المقال، هذا شرط إلزامي وليس اختيارياً. وزّع ذكر كل كلمة ثانوية على أقسام مختلفة من المقال.
الكلمات المفتاحية الثانوية المطلوبة هي: {secondary_list}
5. الوصول إلى التكرار المطلوب للكلمة المفتاحية الأساسية هو شرط فني صارم لا تتنازل عنه أبداً.
6. ⚠️ قاعدة الخلاصة الصارمة: قسم الخلاصة تحديداً يجب ألا يحتوي على الكلمة المفتاحية الأساسية أكثر من مرة واحدة بالضبط في كامل القسم، بغض النظر عن طول القسم أو عدد فقراته. هذا حد أقصى صارم خاص بقسم الخلاصة فقط، ولا ينطبق بنفس الصرامة على باقي الأقسام.

"""
    if is_rewrite:
        if rewrite_notes and rewrite_notes.strip():
            prompt += f"\nلاحظ المستخدم على المحاولة السابقة ما يلي: '{rewrite_notes.strip()}'. عند إعادة كتابة المقال، عالج هذه الملاحظة تحديداً مع الحفاظ على كل القواعد الأخرى (السيو، الهيكلة، الأسلوب البشري).\n"
        prompt += "\nأعد كتابة هذا المقال من جديد بالكامل بأسلوب صياغة وكلمات مختلفة تماماً عن أي محاولة سابقة، مع الحفاظ على نفس جميع القواعد المذكورة أعلاه.\n"
    
    prompt += """

## ✅ مراجعة ذاتية قبل الإرسال (Self-Check):
بعد الانتهاء من كتابة المقال كاملاً، وقبل إرسال إجابتك، راجعه مقابل هذه القائمة وصحّح أي مخالفة الآن:
1. هل استخدمت العنوان الرئيسي (H1) وكل عنوان فرعي من الـ Outline حرفياً كما ورد دون أي تعديل أو إضافة؟
2. هل تجنبت جميع العبارات الممنوعة ('بالإضافة إلى ذلك'، 'علاوة على ذلك'، 'فضلاً عن ذلك'، 'في الختام'، 'يعد من أهم'، 'لا يخفى على أحد'، 'من الجدير بالذكر')؟
3. هل قسم الخلاصة يحتوي على الكلمة المفتاحية الأساسية مرة واحدة فقط كحد أقصى؟
4. هل الأسلوب العام يبدو طبيعياً وبشرياً، لا مصطنعاً أو قولباً؟
5. راجع تصريف كل فعل يأتي بعد 'مما' أو 'الذي' أو 'التي' بدقة: يجب أن يكون الفعل مضارعاً مرفوعاً متفقاً مع الفاعل (مثال صحيح: 'مما يحسّن'، 'مما يعزز' - مثال خاطئ: 'مما تحسين'، 'مما تعزيز'). اقرأ كل جملة تحتوي على هذه الروابط مرتين قبل إنهاء الفقرة للتأكد من صحة الصياغة النحوية.
إذا وجدت أي مخالفة لأي من هذه النقاط، صحّحها قبل إرسال الإجابة النهائية.
"""
    prompt += "\nابدأ الكتابة الآن:"

    try:
        article = call_gemini(prompt, keys)
        article = re.sub(r'\n\n\*\*\*\n\n### تقرير السيو[\s\S]*$', '', article)

        escaped_keyword = re.escape(primary_keyword)
        strict_reminder = "\n\nتذكير صارم: يُمنع تماماً إضافة الكلمة المفتاحية كجملة ختامية لأي فقرة أثناء هذا التعديل. لا تضف الكلمة المفتاحية في أكثر من جملة واحدة ضمن أي فقرة واحدة. إذا كانت فقرة معينة تحتوي بالفعل على الكلمة المفتاحية، لا تضف تكراراً إضافياً لها في نفس الفقرة حتى لو كنت تضيف جملاً جديدة إليها."

        # ── 0. Auto-correction: Conclusion specific rule ──────────────────────────
        sections = re.split(r'(?=\n## )', article)
        conclusion_corrected = False
        for i, section in enumerate(sections):
            if 'الخلاصة' in section:
                conc_keyword_count = len(re.findall(escaped_keyword, section, re.IGNORECASE))
                if conc_keyword_count > 1:
                    print(f"[generate] قسم الخلاصة يحتوي على {conc_keyword_count} تكرارات للكلمة المفتاحية، جاري التصحيح المخصص...")
                    conc_prompt = f"قسم الخلاصة هذا يحتوي على الكلمة المفتاحية '{primary_keyword}' عدد {conc_keyword_count} مرات. أعد صياغته بحيث تظهر الكلمة المفتاحية مرة واحدة فقط كحد أقصى، مع الحفاظ على نفس المعنى والرسالة الختامية. أرسل نص القسم المُعاد صياغته فقط.\n\nالنص الحالي:\n{section}"
                    try:
                        corrected_conc = call_gemini(conc_prompt, keys, temperature=0.5, max_tokens=2000)
                        if corrected_conc:
                            corrected_conc = re.sub(r'\n\n\*\*\*\n\n### تقرير السيو[\s\S]*$', '', corrected_conc)
                            sections[i] = "\n" + corrected_conc.strip()
                            article = "".join(sections)
                            conclusion_corrected = True
                            print("[generate] ✅ تم تصحيح قسم الخلاصة بنجاح")
                    except Exception as e:
                        print(f"[generate] فشل تصحيح قسم الخلاصة: {e}")
                break

        # ── 1. Auto-correction: word count (Section-based)
        words = [w for w in article.strip().split() if w]
        article_wc_before = len(words)
        wc_diff = article_wc_before - target_wc
        was_wc_corrected = False
        wc_reached_target = True  # سيتم تحديثه إذا فشل التصحيح
        wc_before_correction = article_wc_before

        if abs(wc_diff) > 30:
            print(f"[generate] عدد الكلمات ({article_wc_before}) بعيد عن الهدف ({target_wc})، جاري التصحيح الموجه...")
            was_wc_corrected = True

            parsed_sections = re.split(r'(?=\n## )', article)
            candidates = []
            for idx, text in enumerate(parsed_sections):
                s_words = len([w for w in text.strip().split() if w])
                if '## ' in text and 'الخلاصة' not in text and 'نظرة سريعة' not in text:
                    candidates.append({"text": text, "idx": idx, "words": s_words})

            candidates.sort(key=lambda x: x["words"], reverse=True)
            num_targets = 2 if abs(wc_diff) > 300 else 1
            targets = candidates[:num_targets]

            if targets:
                diff_per_section = round(abs(wc_diff) / len(targets))
                is_expansion = wc_diff < 0
                min_required_gain = round(diff_per_section * 0.70)  # 70% من الزيادة المطلوبة

                for target in targets:
                    original_words = target['words']
                    best_text = target['text']  # الاحتفاظ بأفضل نسخة
                    best_words = original_words

                    for attempt in range(1, 3):  # محاولتان كحد أقصى
                        if attempt == 1:
                            if is_expansion:
                                wc_prompt = f"وسّع هذا القسم تحديداً بإضافة {diff_per_section} كلمة فقط (بهامش ±10 كلمات لا أكثر) من محتوى ذي قيمة حقيقية. لا تُضف الكلمة المفتاحية '{primary_keyword}' أكثر من مرة واحدة إضافية في هذا القسم. أرسل نص القسم الموسّع فقط.\n\nالقسم الحالي:\n{target['text']}"
                            else:
                                wc_prompt = f"اختصر هذا القسم تحديداً بإزالة {diff_per_section} كلمة تقريباً، عبر حذف التكرار أو الإطناب غير الضروري، دون حذف أي معلومة جوهرية. أرسل نص القسم المُختصر فقط.\n\nالقسم الحالي:\n{target['text']}"
                        else:
                            if is_expansion:
                                wc_prompt = f"المحاولة السابقة لم تُضف كلمات كافية. يجب أن يكون القسم الجديد أطول بشكل ملموس من النسخة الأصلية بما لا يقل عن {min_required_gain} كلمة جديدة. أضف تفاصيل عملية وأمثلة واقعية في هذا القسم. الهدف: إضافة {diff_per_section} كلمة (بهامش ±10). احتفظ بجميع المعلومات الأصلية ولا تستخدم العبارات النمطية الممنوعة. أرسل نص القسم الموسّع فقط.\n\nالقسم الأصلي:\n{target['text']}"
                            else:
                                wc_prompt = f"المحاولة السابقة لم تختصر الكلمات الكافية. يجب حذف {diff_per_section} كلمة تقريباً من هذا القسم عبر إزالة الإطناب والتكرار، دون حذف أي معلومة جوهرية. أرسل نص القسم المُختصر فقط.\n\nالقسم الأصلي:\n{target['text']}"

                        try:
                            wc_corrected = call_gemini(wc_prompt, keys, temperature=0.5 if attempt == 1 else 0.7, max_tokens=10000)
                            if wc_corrected:
                                wc_corrected = re.sub(r'\n\n\*\*\*\n\n### تقرير السيو[\s\S]*$', '', wc_corrected)
                                new_words = len([w for w in wc_corrected.strip().split() if w])
                                if new_words >= 30:
                                    # التحقق الوظيفي: هل زاد الطول فعلياً بالقدر المطلوب؟
                                    if is_expansion:
                                        actual_gain = new_words - original_words
                                        if actual_gain >= min_required_gain:
                                            best_text = wc_corrected.strip()
                                            best_words = new_words
                                            print(f"[generate] ✅ تصحيح القسم {target['idx']} (محاولة {attempt}): {original_words}{new_words} (+{actual_gain} كلمة)")
                                            break  # نجاح — لا داعي لمحاولة ثانية
                                        else:
                                            print(f"[generate] ⚠️ محاولة {attempt}: القسم زاد {actual_gain} كلمة فقط (المطلوب: {min_required_gain}{'إعادة محاولة...' if attempt == 1 else 'الاحتفاظ بالأفضل'}")
                                            if new_words > best_words:
                                                best_text = wc_corrected.strip()
                                                best_words = new_words
                                    else:  # اختصار
                                        actual_reduction = original_words - new_words
                                        if actual_reduction >= min_required_gain:
                                            best_text = wc_corrected.strip()
                                            best_words = new_words
                                            print(f"[generate] ✅ تصحيح القسم {target['idx']} (محاولة {attempt}): {original_words}{new_words} (-{actual_reduction} كلمة)")
                                            break
                                        else:
                                            print(f"[generate] ⚠️ محاولة {attempt}: القسم اختُصر {actual_reduction} كلمة فقط (المطلوب: {min_required_gain})")
                                            if new_words < best_words:
                                                best_text = wc_corrected.strip()
                                                best_words = new_words
                                else:
                                    print(f"[generate] ⚠️ محاولة {attempt}: استجابة قصيرة جداً ({new_words} كلمة) — تم تجاهلها.")
                        except Exception as e:
                            print(f"[generate] فشل تصحيح القسم {target['idx']} (محاولة {attempt}): {e}")
                            break

                    # استخدام أفضل نسخة حصلنا عليها
                    parsed_sections[target['idx']] = "\n" + best_text

                article = "".join(parsed_sections)

                # تحقق نهائي: هل وصلنا للهدف فعلياً؟
                final_wc_after_section_corr = len([w for w in article.strip().split() if w])
                if abs(final_wc_after_section_corr - target_wc) > 30:
                    wc_reached_target = False
                    print(f"[generate] ⚠️ تصحيح عدد الكلمات: لم يصل للهدف الكامل ({final_wc_after_section_corr} بدلاً من {target_wc})")
            else:
                wc_reached_target = False
                print("[generate] ⚠️ لم يتم العثور على أقسام مناسبة للتوسيع الموجه")

        # ── 2. Recalculate target limits based on ACTUAL stable word count
        stable_words = [w for w in article.strip().split() if w]
        stable_wc = len(stable_words)
        dyn_min = math.ceil(stable_wc * 0.01)
        dyn_max = math.floor(stable_wc * 0.015)
        dyn_min_sec = max(3, round((stable_wc / 900.0) * 3))

        # ── 3. Auto-correction: keyword density
        keyword_count = len(re.findall(escaped_keyword, article, re.IGNORECASE))
        was_kw_added = False
        was_kw_reduced = False

        if keyword_count < dyn_min:
            print(f"[generate] عدد التكرارات ({keyword_count}) أقل من الحد ({dyn_min})، جاري الإضافة...")
            was_kw_added = True
            diff = dyn_min - keyword_count
            corr_prompt = f"هذا المقال يحتوي على {keyword_count} تكرار فقط للكلمة المفتاحية '{primary_keyword}'، وهو أقل من المطلوب ({dyn_min} على الأقل). أضف {diff} تكرارات إضافية للكلمة في فقرات مناسبة من المقال، مع الالتزام الصارم بكل القواعد التالية أثناء الإضافة:\n- يجب أن تندمج الإضافة بشكل طبيعي 100% ضمن سياق الجملة، وتقدم قيمة حقيقية للقارئ وليست مجرد حشو.\n- لا تستخدم أياً من العبارات النمطية الممنوعة (بالإضافة إلى ذلك، علاوة على ذلك، في الختام، إلخ).\n- لا تضف الكلمة كجملة ختامية لأكثر من قسم متتالٍ.\n- لا تُعدّل أو تحذف أي معلومة موجودة في النص الأصلي.\nأرسل النص الكامل المُعدّل فقط.\n{strict_reminder}\n\nالمقال الحالي:\n{article}\n"
            # حد الأمان: 20% من targetWordCount كحد أدنى للمقال الكامل (لا يقل عن 200)
            full_article_min_wc = max(200, round(target_wc * 0.20))
            try:
                kw_corrected = call_gemini(corr_prompt, keys, temperature=0.7)
                # ── حماية أمان: لا نستبدل المقال إلا إذا كانت الاستجابة
                # ── تحتوي على نص حقيقي (≥ 20% من targetWC) يشير لمقال كامل
                if kw_corrected:
                    candidate = re.sub(r'\n\n\*\*\*\n\n### تقرير السيو[\s\S]*$', '', kw_corrected)
                    candidate_wc = len(candidate.strip().split())
                    if candidate_wc >= full_article_min_wc:
                        article = candidate
                        keyword_count = len(re.findall(escaped_keyword, article, re.IGNORECASE))
                    else:
                        print(f"[generate] ⚠️ استجابة تصحيح الإضافة قصيرة ({candidate_wc} < {full_article_min_wc} كلمة) — تم تجاهلها للحفاظ على المقال الأصلي.")
            except Exception as e:
                print(f"[generate] فشل إضافة الكلمات المفتاحية: {e}")

        elif keyword_count > dyn_max:
            print(f"[generate] الكلمة المفتاحية تجاوزت الحد الأقصى ({keyword_count} > {dyn_max})، جاري الحذف...")
            was_kw_reduced = True
            diff = keyword_count - dyn_max
            corr_prompt = f"هذا المقال يحتوي على {keyword_count} تكرار للكلمة المفتاحية '{primary_keyword}'، وهو أكثر من الحد الأقصى المسموح به ({dyn_max}). احذف {diff} تكرارات زائدة لهذه الكلمة المفتاحية مع الحفاظ على المعنى وصياغة الجملة بشكل طبيعي. استهدف تحديداً الفقرات التي تحتوي على أكثر من ذكر واحد للكلمة المفتاحية، أو الجمل الختامية في نهاية الفقرات. أرسل النص الكامل المُعدّل فقط.\n\nالمقال الحالي:\n{article}"
            try:
                kw_corrected = call_gemini(corr_prompt, keys, temperature=0.5)
                # ── حماية أمان: نفس الـ guard للحذف (نفس الحد المحسوب أعلاه)
                if kw_corrected:
                    candidate = re.sub(r'\n\n\*\*\*\n\n### تقرير السيو[\s\S]*$', '', kw_corrected)
                    candidate_wc = len(candidate.strip().split())
                    if candidate_wc >= full_article_min_wc:
                        article = candidate
                        keyword_count = len(re.findall(escaped_keyword, article, re.IGNORECASE))
                    else:
                        print(f"[generate] ⚠️ استجابة تصحيح الحذف قصيرة ({candidate_wc} < {full_article_min_wc} كلمة) — تم تجاهلها للحفاظ على المقال الأصلي.")
            except Exception as e:
                print(f"[generate] فشل حذف الكلمات المفتاحية: {e}")

        # ── 3.5. Auto-correction: secondary keyword density
        was_sec_kw_added = False
        sec_kw_reached_target = True  # سيتم تحديثه إذا فشل التصحيح
        if sec_keys:
            missing_sec_kws = []
            for kw in sec_keys:
                c = len(re.findall(re.escape(kw), article, re.IGNORECASE))
                if c < dyn_min_sec:
                    missing_sec_kws.append({"kw": kw, "count": c, "diff": dyn_min_sec - c})

            if missing_sec_kws:
                print(f"[generate] بعض الكلمات الثانوية لم تصل للحد ({dyn_min_sec})، جاري الإضافة...")
                was_sec_kw_added = True
                best_article_for_sec = article  # الاحتفاظ بأفضل نسخة

                def count_sec_improvements(text, missing_list, min_sec):
                    """يُعيد عدد الكلمات الثانوية التي تحسّنت (أي زاد عددها مقارنةً بالقديم)"""
                    improved = 0
                    for item in missing_list:
                        new_c = len(re.findall(re.escape(item['kw']), text, re.IGNORECASE))
                        if new_c > item['count']:
                            improved += 1
                    return improved

                best_improvements = count_sec_improvements(best_article_for_sec, missing_sec_kws, dyn_min_sec)

                for attempt in range(1, 3):  # محاولتان كحد أقصى
                    if attempt == 1:
                        instructions = "\n".join([
                            f"- الكلمة '{item['kw']}': العدد الحالي {item['count']}، المطلوب {dyn_min_sec} (أضف {item['diff']} تكرارات)."
                            for item in missing_sec_kws
                        ])
                        sec_prompt = f"هذا المقال ينقصه بعض التكرارات للكلمات المفتاحية الثانوية. المطلوب منك إضافة هذه التكرارات الناقصة في فقرات مناسبة من المقال:\n{instructions}\n\nيجب الالتزام الصارم بما يلي:\n- ادمج الإضافة بشكل طبيعي 100% ضمن سياق الجملة لتقدم قيمة حقيقية.\n- لا تستخدم العبارات النمطية الممنوعة (بالإضافة إلى ذلك، في الختام، إلخ).\n- لا تُعدّل أو تحذف أي معلومة موجودة في النص الأصلي.\n- لا تخل بتكرار الكلمة المفتاحية الأساسية '{primary_keyword}'.\nأرسل النص الكامل المُعدّل فقط.\n\nالمقال الحالي:\n{article}\n"
                    else:
                        # تحديث قائمة الناقصة بعد المحاولة الأولى
                        still_missing = [
                            {"kw": item['kw'], "count": len(re.findall(re.escape(item['kw']), best_article_for_sec, re.IGNORECASE)), "diff": 0}
                            for item in missing_sec_kws
                        ]
                        still_missing = [
                            {**item, "diff": dyn_min_sec - item['count']}
                            for item in still_missing if item['count'] < dyn_min_sec
                        ]
                        if not still_missing:
                            break  # كل الكلمات تحسّنت في المحاولة الأولى
                        instructions2 = "\n".join([
                            f"- الكلمة '{item['kw']}': لا تزال عند {item['count']} تكرارات فقط، تحتاج {item['diff']} تكرارات إضافية. يجب إضافتها صراحةً في جمل موضوعية جديدة."
                            for item in still_missing
                        ])
                        sec_prompt = f"المحاولة السابقة لم تُضف جميع الكلمات الثانوية الناقصة بشكل كافٍ. هذه المرة يجب أن تُضاف هذه الكلمات صراحةً كجزء من جمل ذات معنى حقيقي:\n{instructions2}\n\n- لا تُعدّل أو تحذف أي معلومة موجودة في النص الأصلي.\n- لا تخل بتكرار الكلمة المفتاحية الأساسية '{primary_keyword}'.\n- لا تستخدم العبارات النمطية الممنوعة.\nأرسل النص الكامل المُعدّل فقط.\n\nالمقال الحالي:\n{best_article_for_sec}\n"

                    try:
                        sec_kw_corrected = call_gemini(sec_prompt, keys, temperature=0.7)
                        if sec_kw_corrected:
                            candidate = re.sub(r'\n\n\*\*\*\n\n### تقرير السيو[\s\S]*$', '', sec_kw_corrected)
                            candidate_wc = len(candidate.strip().split())
                            if candidate_wc >= full_article_min_wc:
                                # التحقق الوظيفي: هل زاد تكرار الكلمات الثانوية فعلياً؟
                                improvements = count_sec_improvements(candidate, missing_sec_kws, dyn_min_sec)
                                if improvements > best_improvements:
                                    best_article_for_sec = candidate
                                    best_improvements = improvements
                                    print(f"[generate] ✅ تصحيح الكلمات الثانوية (محاولة {attempt}): تحسّنت {improvements}/{len(missing_sec_kws)} كلمة")
                                    if improvements == len(missing_sec_kws):
                                        break  # نجاح كامل
                                else:
                                    print(f"[generate] ⚠️ محاولة {attempt}: لم يتحسن تكرار الكلمات الثانوية — {'إعادة محاولة...' if attempt == 1 else 'الاحتفاظ بالأفضل'}")
                            else:
                                print(f"[generate] ⚠️ محاولة {attempt}: استجابة قصيرة ({candidate_wc} < {full_article_min_wc}) — تم تجاهلها.")
                    except Exception as e:
                        print(f"[generate] فشل إضافة الكلمات الثانوية (محاولة {attempt}): {e}")
                        break

                article = best_article_for_sec

                # تحقق نهائي من الكلمات الثانوية
                still_failing = []
                for item in missing_sec_kws:
                    final_c = len(re.findall(re.escape(item['kw']), article, re.IGNORECASE))
                    if final_c < dyn_min_sec:
                        still_failing.append(item['kw'])
                if still_failing:
                    sec_kw_reached_target = False
                    print(f"[generate] ⚠️ الكلمات الثانوية التالية لم تصل للهدف بعد المحاولتين: {still_failing}")

        # ── 4. Final Verification Pass & SEO Report
        final_words = [w for w in article.strip().split() if w]
        final_wc = len(final_words)
        final_kw_count = len(re.findall(escaped_keyword, article, re.IGNORECASE))
        final_density = round((final_kw_count / final_wc * 100), 2) if final_wc > 0 else 0
        final_first_100 = " ".join(final_words[:100])
        final_in_100 = bool(re.search(escaped_keyword, final_first_100, re.IGNORECASE))
        final_wc_diff = final_wc - target_wc
        final_wc_status = "✅" if abs(final_wc_diff) <= 30 else "❌"

        sec_report = ""
        has_sec_warn = False
        if sec_keys:
            sec_report = f"\n*   **تكرار الكلمات المفتاحية الثانوية** (الحد الأدنى المطلوب: {dyn_min_sec} لكل كلمة):\n"
            for kw in sec_keys:
                c = len(re.findall(re.escape(kw), article, re.IGNORECASE))
                status = "✅" if c >= dyn_min_sec else "❌"
                if c < dyn_min_sec: has_sec_warn = True
                sec_report += f"    * {kw}: {c} مرات {status}\n"

        if was_wc_corrected:
            if wc_reached_target:
                wc_corr_status = f"تم بنجاح ✅ ({wc_before_correction}{stable_wc} كلمة)"
            else:
                wc_corr_status = f"تمت المحاولة لكن لم يصل للهدف الكامل ⚠️ ({wc_before_correction}{stable_wc} كلمة)"
        else:
            wc_corr_status = "لم يُحتج إليه ✅"

        if dyn_min <= final_kw_count <= dyn_max:
            kw_status = "✅"
        elif final_kw_count < dyn_min:
            kw_status = "❌"
        else:
            kw_status = "⚠️"

        kw_add_status = "تم تفعيله 🔄" if was_kw_added else "لم يُحتج إليه ✅"
        kw_red_status = "تم تفعيله ✂️" if was_kw_reduced else "لم يُحتج إليه ✅"
        if was_sec_kw_added:
            sec_kw_add_status = "تم بنجاح ✅" if sec_kw_reached_target else "تمت المحاولة لكن لم يصل للهدف الكامل ⚠️"
        else:
            sec_kw_add_status = "لم يُحتج إليه ✅"

        warnings = ""
        if abs(final_wc_diff) > 30:
            sign = "+" if final_wc_diff > 0 else ""
            warnings += f"\n⚠️ **طول المقال:** المقال النهائي يبعد بـ {sign}{final_wc_diff} كلمة عن الهدف ({target_wc})، وهو خارج هامش التفاوت المسموح (±30)."
        if final_kw_count < dyn_min or final_kw_count > dyn_max:
            warnings += f"\n⚠️ **كثافة الكلمة الأساسية:** التكرار النهائي ({final_kw_count}) خارج النطاق المقبول ({dyn_min}-{dyn_max})."
        if has_sec_warn:
            warnings += f"\n⚠️ **الكلمات الثانوية:** بعض الكلمات الثانوية لم تحقق الحد الأدنى من التكرار ({dyn_min_sec})."
        if not final_in_100:
            warnings += f"\n⚠️ **أول 100 كلمة:** الكلمة المفتاحية الأساسية لم تظهر في أول 100 كلمة."

        report = (
            f"\n\n***\n\n### تقرير السيو (محسوب آلياً 🤖)\n"
            f"*   **عدد الكلمات المستهدف:** {target_wc} كلمة\n"
            f"*   **عدد الكلمات الفعلي النهائي:** {final_wc} كلمة {final_wc_status}\n"
            f"*   **الفرق عن الهدف:** {'+' if final_wc_diff > 0 else ''}{final_wc_diff} كلمة\n"
            f"*   **تصحيح عدد الكلمات:** {wc_corr_status}\n"
            f"*   **التكرار الفعلي للكلمة الأساسية:** {final_kw_count} مرات (المطلوب: {dyn_min}{dyn_max}) {kw_status}\n"
            f"*   **تصحيح نقص الكلمة المفتاحية:** {kw_add_status}\n"
            f"*   **تصحيح الكلمات المفتاحية الزائدة:** {kw_red_status}\n"
            f"*   **تصحيح نقص الكلمات الثانوية:** {sec_kw_add_status}\n"
            f"*   **نسبة الكثافة الفعلية النهائية:** {final_density}%\n"
            f"*   **الظهور في أول 100 كلمة:** {'نعم ✅' if final_in_100 else 'لا ❌'}"
            + sec_report
            + (f"\n\n**⚠️ تحذيرات المراجعة النهائية:**{warnings}" if warnings else "")
        )

        # ── 5. Structured Console Log (visible in HuggingFace Logs) ──────────────
        log_seo_report(
            title=title,
            primary_keyword=primary_keyword,
            target_wc=target_wc,
            final_wc=final_wc,
            final_wc_diff=final_wc_diff,
            final_kw_count=final_kw_count,
            dyn_min=dyn_min,
            dyn_max=dyn_max,
            final_density=final_density,
            final_in_100=final_in_100,
            sec_keys=sec_keys,
            dyn_min_sec=dyn_min_sec,
            article=article,
            warnings=warnings,
            was_wc_corrected=was_wc_corrected,
            wc_before_correction=wc_before_correction,
            stable_wc=stable_wc,
            was_kw_added=was_kw_added,
            was_kw_reduced=was_kw_reduced,
            conclusion_corrected=conclusion_corrected,
            was_sec_kw_added=was_sec_kw_added,
        )

        # فحص إضافي للعبارات الممنوعة
        forbidden_phrases = [
            'بالإضافة إلى ذلك',
            'علاوة على ذلك',
            'فضلاً عن ذلك',
            'في الختام',
            'يعد من أهم',
            'لا يخفى على أحد',
            'من الجدير بالذكر'
        ]
        found_phrase = next((p for p in forbidden_phrases if p in article), None)
        forbidden_check = f"العبارات الممنوعة: وُجدت '{found_phrase}' في النص ❌" if found_phrase else "العبارات الممنوعة: لا توجد ✅"
        extra_check_section = f"\n\n### فحص إضافي\n{forbidden_check}"
        report += extra_check_section

        # تقييم وصفي (طلب منفصل)
        eval_prompt = f"""راجع هذا المقال النهائي وقيّمه بصدق حسب المعايير التالية، دون تعديل أي شيء في النص نفسه:
1. الهيكلة الهرمية: هل استُخدمت عناوين H3 بشكل منطقي حيث كانت هناك حاجة فعلية لتقسيم فرعي؟ (نعم / لا / جزئياً - سبب واحد مختصر)
2. استخدام النقاط: هل استُخدمت bullet points في الأماكن المناسبة؟ (نعم / لا / جزئياً)
3. الأسلوب: هل الكتابة بسيطة وطبيعية وخالية من المبالغة ومكتوبة بأسلوب بشري بسيط؟ (نعم / لا / جزئياً)
4. السلامة اللغوية: هل توجد أخطاء نحوية واضحة؟ (اذكرها أو 'لا توجد')
أجب بإيجاز شديد (سطر واحد لكل بند فقط).

المقال:
{article}"""

        suggested_rewrite_notes = None
        try:
            eval_payload = {
                "contents": [{"parts": [{"text": eval_prompt}]}],
                "generationConfig": {"maxOutputTokens": 250, "temperature": 0.1}
            }
            for k in keys:
                try:
                    resp = requests.post(
                        f"https://generativelanguage.googleapis.com/v1beta/models/{MODEL_NAME}:generateContent?key={k}",
                        headers={"Content-Type": "application/json"},
                        json=eval_payload,
                        timeout=10
                    )
                    if resp.status_code == 200:
                        eval_data = resp.json()
                        eval_text = eval_data.get("candidates", [{}])[0].get("content", {}).get("parts", [{}])[0].get("text", "")
                        if eval_text:
                            report += f"\n\n### تقييم وصفي\n{eval_text.strip()}"
                            
                            if "لا" in eval_text or "جزئياً" in eval_text or "جزئيا" in eval_text:
                                rewrite_prompt = f"""بناءً على هذا التقييم لمقال:
{eval_text.strip()}

اكتب فقرة قصيرة جداً (3 أسطر كحد أقصى) بصيغة تعليمات مباشرة لإعادة صياغة المقال، تعالج فقط المشاكل المذكورة في التقييم أعلاه. لا تضف أي قاعدة عامة جديدة، فقط صف بدقة ماذا يجب تصحيحه."""
                                rw_payload = {
                                    "contents": [{"parts": [{"text": rewrite_prompt}]}],
                                    "generationConfig": {"maxOutputTokens": 200, "temperature": 0.3}
                                }
                                for rw_k in keys:
                                    try:
                                        rw_resp = requests.post(
                                            f"https://generativelanguage.googleapis.com/v1beta/models/{MODEL_NAME}:generateContent?key={rw_k}",
                                            headers={"Content-Type": "application/json"},
                                            json=rw_payload,
                                            timeout=10
                                        )
                                        if rw_resp.status_code == 200:
                                            rw_data = rw_resp.json()
                                            rw_text = rw_data.get("candidates", [{}])[0].get("content", {}).get("parts", [{}])[0].get("text", "")
                                            if rw_text:
                                                suggested_rewrite_notes = rw_text.strip()
                                            break
                                    except Exception:
                                        continue
                        break
                except Exception:
                    continue
        except Exception:
            pass

        return article + report, suggested_rewrite_notes or ""

    except Exception as e:
        return f"❌ خطأ: {str(e)}", ""

# ── Gradio UI ─────────────────────────────────────────────────────────────────
with gr.Blocks(title="اكتب مقالتك بشكل احترافي", theme=gr.themes.Soft(primary_hue="blue", font=gr.themes.GoogleFont("Cairo"))) as demo:
    gr.Markdown(
        """
        # 🚀 اكتب مقالتك بشكل احترافي
        ### أداة متقدمة لكتابة مقالات عربية محسّنة لمحركات البحث باستخدام Gemini AI
        """
    )

    with gr.Row():
        with gr.Column(scale=1):
            title_input = gr.Textbox(label="📝 عنوان المقال", placeholder="مثال: أفضل نظارات شمسية رجالية", rtl=True)
            outline_input = gr.Textbox(label="📋 الـ Outline (عنوان لكل سطر)", placeholder="معايير اختيار النظارة\nأفضل الماركات\nنصائح للعناية", lines=5, rtl=True)
            primary_kw = gr.Textbox(label="🎯 الكلمة المفتاحية الأساسية", placeholder="مثال: نظارات شمسية رجالية", rtl=True)
            secondary_kw = gr.Textbox(label="🔑 الكلمات المفتاحية الثانوية (مفصولة بفاصلة أو شرطة)", placeholder="مثال: نظارات شمسية أصلية, حماية العين", rtl=True)
            
            with gr.Row():
                target_word_count = gr.Number(label="📏 عدد الكلمات المستهدف (800-1500)", value=900, minimum=800, maximum=1500)
            
            is_rewrite = gr.Checkbox(label="🔄 إعادة صياغة لمحاولة سابقة", value=False, interactive=True)
            rewrite_notes = gr.Textbox(label="✍️ ملاحظات لإعادة الصياغة (اختياري)", placeholder="مثال: أضف فقرة عن كذا وكذا...", lines=2, rtl=True, visible=False)

            generate_btn = gr.Button("✨ توليد المقال", variant="primary", size="lg")

        with gr.Column(scale=2):
            output = gr.Textbox(label="📄 المقال المُولَّد + تقرير السيو", lines=35, rtl=True, elem_classes=["output-box"], elem_id="article-output")
            copy_btn = gr.Button("📋 نسخ النص", variant="secondary", size="sm", elem_id="copy-btn")

    generate_btn.click(
        fn=generate_article,
        inputs=[title_input, outline_input, primary_kw, secondary_kw, target_word_count, is_rewrite, rewrite_notes],
        outputs=[output, rewrite_notes],
    )

    is_rewrite.change(
        fn=lambda checked: gr.update(visible=checked),
        inputs=[is_rewrite],
        outputs=[rewrite_notes],
    )

    copy_btn.click(
        fn=None,
        inputs=[],
        outputs=[],
        js="""
        async () => {
            const textarea = document.querySelector('#article-output textarea');
            if (!textarea || !textarea.value.trim()) return;
            const text = textarea.value;
            
            let html = text.replace(/</g, '&lt;').replace(/>/g, '&gt;');
            html = html.replace(/^### (.*?)$/gm, '<h3>$1</h3>');
            html = html.replace(/^## (.*?)$/gm, '<h2>$1</h2>');
            html = html.replace(/^# (.*?)$/gm, '<h1>$1</h1>');
            html = html.replace(/\\*\\*(.*?)\\*\\*/g, '<strong>$1</strong>');
            html = html.replace(/^[\\*\\-] (.*?)$/gm, '<li>$1</li>');
            html = html.replace(/(<li>.*?<\\/li>(?:\\n<li>.*?<\\/li>)*)/g, '<ul>$1</ul>');
            html = html.split('\\n\\n').map(p => {
                if (p.startsWith('<h') || p.startsWith('<ul>')) return p;
                return '<p>' + p.replace(/\\n/g, '<br>') + '</p>';
            }).join('');

            const div = document.createElement('div');
            div.innerHTML = html;
            div.style.position = 'fixed';
            div.style.left = '-9999px';
            div.style.color = '#000000';
            div.style.backgroundColor = '#ffffff';
            div.dir = 'rtl';
            document.body.appendChild(div);
            
            const range = document.createRange();
            range.selectNodeContents(div);
            const sel = window.getSelection();
            sel.removeAllRanges();
            sel.addRange(range);
            
            try {
                document.execCommand('copy');
            } catch (err) {
                navigator.clipboard.writeText(text);
            }
            
            sel.removeAllRanges();
            document.body.removeChild(div);
            
            const btn = document.querySelector('#copy-btn');
            if (btn) {
                const original = btn.innerText;
                btn.innerText = '✅ تم النسخ!';
                setTimeout(() => { btn.innerText = original; }, 2000);
            }
        }
        """,
    )

    gr.Markdown(
        """
        ---
        **ملاحظات:**
        - يتم حساب تقرير السيو آلياً ويحتوي على نظام تصحيح ذاتي لضمان الكثافة والطول.
        - النظام يدعم 3 مفاتيح API مع انتقال تلقائي عند استنفاد الحصة.
        """
    )

demo.launch(
    css="""
        .output-box { font-family: 'Cairo', sans-serif; direction: rtl; text-align: right; }
        h1 { text-align: center; }
    """
)