import os, json, asyncio, re, shutil, sqlite3, urllib.parse, random, logging
from datetime import datetime, timedelta
from fastapi import FastAPI, Query
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
from playwright.async_api import async_playwright

logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
logger = logging.getLogger(__name__)

app = FastAPI()
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3006", "http://127.0.0.1:3006"],
    allow_methods=["POST"],
    allow_headers=["*"],
)
PORT = int(os.getenv("PORT", "3008"))

OLLAMA_URL = os.getenv("OLLAMA_URL", "http://localhost:11434")
CLASSIFY_MODEL = os.getenv("CLASSIFY_MODEL", "dolphin-llama3:8b")

FX_PROFILE = os.getenv('FX_PROFILE', '')

BROAD_KEYWORDS = [
    "website", "web design", "web develop", "web dev",
    "build my website", "build a website", "create a website",
    "need web", "looking for web", "new website",
    "landing page", "wordpress",
    "need a website", "my website", "business website",
    "need a designer", "help with my website",
    "redesign", "update my website",
    "looking for", "need a", "need an", "looking to",
    "help me build", "create my", "build me",
    "design my", "make me a", "would like",
    "need someone", "hire a", "looking to hire",
    "want someone", "need help with",
]

OFFER_PATTERNS = [
    r"\bfree\s+domain\b",
    r"\bweb\s+hosting\b",
    r"\bfree\s+website\b",
    r"\bprofessional\s+website",
    r"\baffordable\s+web\b",
    r"\bget\s+a\s+free\b",
    r"\bsee\s+more\b",
    r"\bbook\s+your\b",
    r"\bhire\s+a\s+freelancer\b",
    r"\bcontent\s+strategy\b",
    r"\bdigital\s+marketing\b",
    r"\bseo\s+service",
    r"\bsocial\s+media\b",
    r"\blimited\s+time\s+offer\b",
    r"\bspecial\s+offer\b",
    r"\bsign\s+up\s+now\b",
    r"\br\d[\d,.]",
    r"\bstarting\s+at\b",
    r"\bper\s+month\b",
    r"\bmonthly\b",
    r"\bfreelancer\s+marketplace\b",
    r"\bfirst\s+order\b",
    r"\bdomain\s+name\b",
    r"\bregister\s+your\s+domain\b",
    r"\bsponsored\b",
    r"\bpromoted\b",
    r"\badvertisement\b",
]

REQUEST_PATTERNS = [
    r"\bi\s+need\b",
    r"\bi\s+need\s+someone\b",
    r"\bi\s+need\s+a\b",
    r"\bi\s+need\s+an\b",
    r"\bi\s+want\b",
    r"\bi\s+would\s+like\b",
    r"\bi'm\s+looking\b",
    r"\bi\s+am\s+looking\b",
    r"\blooking\s+for\b",
    r"\blooking\s+to\b",
    r"\bwho\s+can\b",
    r"\bcan\s+someone\b",
    r"\bhelp\s+me\b",
    r"\bneed\s+help\b",
    r"\bwould\s+like\s+to\b",
    r"\bwant\s+to\b",
    r"\banyone\s+know\b",
    r"\bdoes\s+anyone\b",
    r"\brecommend\b",
    r"\bsuggestion\b",
    r"\bquote\s+(please|for|to)\b",
]

def kw_match(text: str) -> bool:
    t = text.lower()
    return any(kw in t for kw in BROAD_KEYWORDS)

def is_request(text: str) -> bool:
    t = text.lower()
    return any(re.search(p, t) for p in REQUEST_PATTERNS)

def is_offer(text: str) -> bool:
    t = text.lower()
    return any(re.search(p, t) for p in OFFER_PATTERNS)

FB_SEARCHES = [
    "looking for web developer",
    "need a website designed",
    "need website built South Africa",
    "need someone to build my website",
    "need web designer",
    "looking for someone to create website",
    "need ecommerce website built",
    "want to hire web developer",
    "need wordpress website",
    "I need a website for my business",
    "need a site for my business",
    "looking for website designer South Africa",
    "need website for my small business",
    "who can build me a website",
    "want to create a website for my business",
    "looking for affordable web design",
    "need a web developer South Africa",
    "help me build a website",
    "need someone to design my website",
    "looking for web designer near me",
    "need a website for my startup",
]

VIEWPORTS = [
    {'width': 1280, 'height': 800},
    {'width': 1366, 'height': 768},
    {'width': 1440, 'height': 900},
    {'width': 1536, 'height': 864},
    {'width': 1920, 'height': 1080},
]

async def get_fb_cookies(profile_path: str | None = None):
    cookie_db_path = profile_path or FX_PROFILE
    if not cookie_db_path:
        logger.warning("No profile path provided")
        return []
    cookie_db = os.path.join(cookie_db_path, 'cookies.sqlite')
    if not os.path.exists(cookie_db):
        logger.warning("Cookie DB not found at %s", cookie_db)
        return []
    tmp = os.path.join(os.path.dirname(cookie_db), f'cookies_tmp_{os.getpid()}.sqlite')
    for attempt in range(3):
        try:
            shutil.copy2(cookie_db, tmp)
            break
        except PermissionError:
            if attempt < 2:
                await asyncio.sleep(0.5)
            else:
                logger.error("Failed to copy cookie DB after 3 attempts")
                return []
    try:
        conn = sqlite3.connect(tmp)
        c = conn.cursor()
        c.execute("SELECT name, value, host, path FROM moz_cookies WHERE host LIKE '%facebook.com'")
        rows = c.fetchall()
        conn.close()
        try:
            os.remove(tmp)
        except Exception:
            pass
        return [{
            "name": name, "value": value,
            "domain": host if host.startswith('.') else f'.{host}',
            "path": path,
            "httpOnly": True, "secure": True, "sameSite": "Lax",
        } for name, value, host, path in rows]
    except Exception as e:
        logger.error("Cookie DB read error: %s", e)
        try:
            os.remove(tmp)
        except Exception:
            pass
        return []

WEEKDAY_ORDER = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']

def _parse_fb_date(block: list[str]) -> str:
    for l in block:
        l = l.strip()
        m = re.match(r'(\d+)\s*(h|hr|hrs|hour|hours)\s*ago', l)
        if m:
            hours = int(m.group(1))
            d = datetime.now() - timedelta(hours=hours)
            return d.strftime('%Y-%m-%d')
        if l.lower() == 'yesterday':
            d = datetime.now() - timedelta(days=1)
            return d.strftime('%Y-%m-%d')
        low = l.lower()
        if low in WEEKDAY_ORDER:
            day_idx = WEEKDAY_ORDER.index(low)
            today_idx = datetime.now().weekday()
            days_ago = (today_idx - day_idx) % 7
            d = datetime.now() - timedelta(days=days_ago)
            return d.strftime('%Y-%m-%d')
        for fmt in ['%Y-%m-%d', '%d %B %Y', '%B %d %Y', '%d %b %Y', '%b %d %Y', '%d %b', '%b %d']:
            try:
                dt = datetime.strptime(l, fmt)
                return dt.strftime('%Y-%m-%d')
            except ValueError:
                pass
    return datetime.now().strftime('%Y-%m-%d')

def _clean_fb_text(text: str) -> str:
    cleaned_lines = []
    for l in text.split('\n'):
        stripped = l.strip()
        if not stripped:
            continue
        if len(stripped) < 3:
            continue
        if all(not c.isalpha() for c in stripped):
            continue
        cleaned_lines.append(stripped)
    return '\n'.join(cleaned_lines)

def _extract_posts_from_elements(elements: list[dict], base_url: str) -> list[dict]:
    posts = []
    seen_texts = set()
    for el in elements:
        raw_text = el.get('text', '')
        if len(raw_text) < 40:
            continue
        text = _clean_fb_text(raw_text)
        if len(text) < 40:
            continue
        if is_offer(text):
            continue
        lines = text.split('\n')
        dekey = text[:80]
        if dekey in seen_texts:
            continue
        seen_texts.add(dekey)
        request_score = 2 if is_request(text) else 0
        post_url = el.get('url') or base_url
        posts.append({
            "title": text[:300],
            "content": text[:1000],
            "author": el.get('author', ''),
            "url": post_url,
            "source": "facebook",
            "date": _parse_fb_date(lines),
            "_score": request_score,
        })
    posts.sort(key=lambda p: p.get('_score', 0), reverse=True)
    for p in posts:
        p.pop('_score', None)
    return posts

def _extract_posts_from_text(raw: str, url: str) -> list[dict]:
    lines = [l.strip() for l in raw.split('\n') if l.strip() and len(l.strip()) >= 15]
    posts = []
    seen_texts = set()
    cur = []
    for l in lines:
        words = re.findall(r'[A-Za-z]{2,}', l)
        if len(words) < 2:
            continue
        lower = l.lower()
        if any(kw in lower for kw in BROAD_KEYWORDS) and not is_offer(l):
            if cur:
                snippet = ' '.join(cur[-3:] + [l])
                if len(snippet) >= 40 and not is_offer(snippet):
                    dekey = snippet[:80]
                    if dekey not in seen_texts:
                        seen_texts.add(dekey)
                        posts.append({
                            "title": snippet[:300],
                            "content": snippet[:1000],
                            "author": '',
                            "url": url,
                            "source": "facebook",
                            "date": _parse_fb_date(cur + [l]),
                        })
                cur = []
            else:
                if not is_offer(l):
                    snippet = l
                    if len(snippet) >= 40:
                        dekey = snippet[:80]
                        if dekey not in seen_texts:
                            seen_texts.add(dekey)
                            posts.append({
                                "title": snippet[:300],
                                "content": snippet[:1000],
                                "author": '',
                                "url": url,
                                "source": "facebook",
                                "date": _parse_fb_date([l]),
                            })
        cur.append(l)
        if len(cur) > 10:
            cur.pop(0)
    return posts

async def human_scroll(page, steps: int = None, total_delay: float = None):
    steps = steps or random.randint(2, 5)
    total_delay = total_delay or random.uniform(6, 18)
    step_delay = total_delay / steps
    for i in range(steps):
        scroll_dist = random.randint(200, 600)
        await page.evaluate(f"window.scrollBy(0, {scroll_dist})")
        await page.wait_for_timeout(int(step_delay * 1000))
    if random.random() < 0.3:
        await page.evaluate(f"window.scrollBy(0, -{random.randint(100, 300)})")
        await page.wait_for_timeout(random.randint(1000, 3000))

async def random_idle(page):
    action = random.random()
    if action < 0.15:
        try:
            elems = await page.query_selector_all('a, span, div[role="button"]')
            if elems:
                el = random.choice(elems[:20])
                box = await el.bounding_box()
                if box:
                    x = box['x'] + box['width'] / 2 + random.randint(-20, 20)
                    y = box['y'] + box['height'] / 2 + random.randint(-10, 10)
                    await page.mouse.move(x, y, steps=random.randint(20, 40))
                    await page.wait_for_timeout(random.randint(500, 2000))
        except Exception:
            pass

async def _get_article_elements(page) -> list[dict]:
    return await page.evaluate('''() => {
        const results = [];
        const seenTexts = new Set();
        const selectors = [
            'div[role="article"]',
            'div[role="feed"] > div',
            'div.x1yztbdb',
            'div[data-pagelet]',
        ];
        for (const sel of selectors) {
            const els = document.querySelectorAll(sel);
            for (const el of els) {
                const text = (el.innerText || '').trim();
                if (text.length < 40) continue;
                const key = text.substring(0, 80);
                if (seenTexts.has(key)) continue;
                seenTexts.add(key);
                const links = el.querySelectorAll('a');
                let author = '';
                for (const a of links) {
                    const t = (a.innerText || '').trim();
                    if (t && t.length > 2 && t.length < 80 && /^[a-zA-ZÀ-ÿ][a-zA-ZÀ-ÿ .'-]+$/.test(t)) {
                        author = t;
                        break;
                    }
                }
                let postUrl = '';
                for (const a of links) {
                    const h = (a.getAttribute('href') || '');
                    if (h.includes('/posts/') || h.includes('/photo/') || h.includes('/video/') || h.includes('/groups/')) {
                        postUrl = h.startsWith('http') ? h : 'https://www.facebook.com' + h;
                        break;
                    }
                }
                results.push({ text, author, url: postUrl });
            }
            if (results.length > 0) break;
        }
        return results;
    }''')

async def search_facebook(page, query: str) -> list[dict]:
    url = f'https://www.facebook.com/search/posts/?q={urllib.parse.quote(query)}'
    try:
        await page.goto(url, wait_until='domcontentloaded', timeout=30000)
        try:
            await page.wait_for_selector('div[role="article"], div[role="feed"]', timeout=15000)
        except Exception:
            pass
        await page.wait_for_timeout(random.randint(3000, 8000))

        await human_scroll(page, steps=random.randint(2, 4), total_delay=random.uniform(6, 15))

        if random.random() < 0.1:
            await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
            await page.wait_for_timeout(random.randint(1000, 3000))
            await page.evaluate("window.scrollBy(0, -300)")
            await page.wait_for_timeout(random.randint(1000, 3000))

        if random.random() < 0.2:
            await page.evaluate("window.scrollTo(0, 0)")
            await page.wait_for_timeout(random.randint(2000, 5000))

        if random.random() < 0.3:
            await random_idle(page)

        raw_articles = await _get_article_elements(page)
        posts = _extract_posts_from_elements(raw_articles, url) if raw_articles else []
        if not posts:
            raw = await page.evaluate('document.body.innerText')
            posts = _extract_posts_from_text(raw, url)
    except Exception as e:
        logger.warning("Facebook search failed: %s", e)
        return []
    return posts

def cleanup_chrome():
    import subprocess, signal
    try:
        subprocess.run(["taskkill", "/F", "/IM", "chrome-headless-shell.exe"], capture_output=True, timeout=5)
    except Exception:
        pass

async def scrape_facebook(profile_path: str | None = None, force: bool = False) -> dict:
    cleanup_chrome()
    fb_cookies = await get_fb_cookies(profile_path)
    if not fb_cookies:
        logger.warning("No Facebook cookies available")
        return {"success": False, "leads": [], "flagged": False, "flag_reason": None, "error": "No cookies available"}
    try:
        async with async_playwright() as pw:
            browser = await pw.chromium.launch(
                headless=True,
                args=['--disable-blink-features=AutomationControlled']
            )
            viewport = random.choice(VIEWPORTS)
            context = await browser.new_context(
                user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0',
                viewport=viewport,
            )

            await context.add_init_script("""
                Object.defineProperty(navigator, 'webdriver', { get: () => false });
                Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
                Object.defineProperty(navigator, 'platform', { get: () => 'Win32' });
                window.chrome = { runtime: {} };
                Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 8 });
                Object.defineProperty(navigator, 'deviceMemory', { get: () => 8 });
            """)
            for c in fb_cookies:
                try:
                    await context.add_cookies([c])
                except Exception as e:
                    logger.warning("Failed to inject cookie %s: %s", c.get('name'), e)

            page = await context.new_page()
            # Navigate through google first for a legitimate Referer header
            await page.goto('https://www.google.com/', wait_until='domcontentloaded', timeout=15000)
            await page.wait_for_timeout(random.randint(1000, 3000))
            await page.goto('https://www.facebook.com/', wait_until='domcontentloaded', timeout=30000)
            await page.wait_for_timeout(random.randint(3000, 8000))

            url = page.url
            if '/login' in url.lower():
                logger.warning("Facebook login page detected — account flagged")
                await browser.close()
                return {"success": False, "leads": [], "flagged": True, "flag_reason": "login_page", "error": "Facebook login page detected"}

            # Browse feed like a human before searching
            await human_scroll(page, steps=random.randint(2, 4), total_delay=random.uniform(8, 20))
            if random.random() < 0.25:
                await page.evaluate("window.scrollTo(0, 0)")
                await page.wait_for_timeout(random.randint(2000, 5000))
                await human_scroll(page, steps=random.randint(1, 2))

            # False start: 30% chance to idle and leave without scraping (skipped when force=true)
            if not force and random.random() < 0.3:
                await page.wait_for_timeout(random.randint(8000, 20000))
                await browser.close()
                return {"success": True, "leads": [], "flagged": False, "flag_reason": None, "error": None}

            all_posts = []
            searches = random.sample(FB_SEARCHES, k=random.randint(5, 8))
            for i, query in enumerate(searches):
                try:
                    posts = await search_facebook(page, query)
                    all_posts.extend(posts)
                    # Between searches: random break with possible small scroll
                    if random.random() < 0.4:
                        await page.evaluate(f"window.scrollBy(0, {random.randint(-300, 300)})")
                    delay = random.uniform(8, 25)
                    await page.wait_for_timeout(int(delay * 1000))
                    # Tab switch: 15% chance after 2nd-3rd search
                    if i == random.randint(1, 2) and random.random() < 0.15:
                        new_page = await context.new_page()
                        await new_page.goto('https://www.messenger.com/', wait_until='domcontentloaded', timeout=15000)
                        await new_page.wait_for_timeout(random.randint(3000, 8000))
                        await new_page.close()
                        await page.wait_for_timeout(random.randint(1000, 3000))
                except Exception as e:
                    logger.warning("Facebook search '%s' failed: %s", query, e)

            # Idle before closing — human would pause
            if random.random() < 0.5:
                await page.wait_for_timeout(random.randint(3000, 10000))

            await browser.close()

            seen = set()
            deduped = []
            for p in all_posts:
                key = p.get('content', '')[:100]
                if key not in seen:
                    seen.add(key)
                    deduped.append(p)

            leads = deduped[:20]
            if leads:
                leads = await classify_leads(leads)

            return {"success": True, "leads": leads[:15], "flagged": False, "flag_reason": None, "error": None}
    except Exception as e:
        logger.error("Facebook scrape failed: %s", e)
        return {"success": False, "leads": [], "flagged": False, "flag_reason": None, "error": str(e)}

async def ask_ollama(prompt: str) -> str:
    import httpx
    async with httpx.AsyncClient(timeout=120) as c:
        r = await c.post(f"{OLLAMA_URL}/api/chat", json={
            "model": CLASSIFY_MODEL,
            "messages": [
                {"role": "system", "content": "You classify forum posts. Return only valid JSON."},
                {"role": "user", "content": prompt}
            ],
            "stream": False,
            "options": {"temperature": 0.05, "num_predict": 1024},
        })
        r.raise_for_status()
        data = r.json()
        return data["message"]["content"]

async def classify_leads(results: list[dict]) -> list[dict]:
    if not results:
        return []
    briefs = [r["title"][:200] for r in results]
    prompt = f"""Classify each post as LEAD or NOT.
LEAD = someone REQUESTING/POSTING/WANTING a website built, designed, or created for them.
LEAD examples: "Need a website for my business", "Looking for web developer to build my site", "I need someone to create my website", "Want a new website for my company", "Looking for someone to design my WordPress site"

NOT LEAD:
- Offering web design services: "I build websites", "I offer web design", "Affordable web design packages"
- Already have a website and need marketing, SEO, content, video, link building, email marketing, affiliates
- Recruiting employees, hiring staff, looking for business partners
- Selling products, promoting services, affiliate offers
- "Need web hosting", "Looking for a partner", "Looking for content writer", "Video spokesperson"

For each numbered post, answer ONLY "yes" (LEAD) or "no" (NOT LEAD):
{chr(10).join(f'{i+1}. {t}' for i, t in enumerate(briefs))}
Return a JSON array like ["yes","no","yes"] matching the order above."""
    ai_succeeded = False
    try:
        raw = await ask_ollama(prompt)
        raw = raw.strip()
        if raw.startswith("```"):
            first_nl = raw.find('\n')
            if first_nl != -1:
                raw = raw[first_nl + 1:]
            if raw.endswith("```"):
                raw = raw[:-3]
            raw = raw.strip()
        answers = json.loads(raw)
        if isinstance(answers, list) and len(answers) == len(results):
            ai_succeeded = True
            filtered = []
            for i, ans in enumerate(answers):
                if isinstance(ans, str) and ans.lower() == 'yes':
                    filtered.append(results[i])
            if filtered:
                return filtered[:10]
            logger.info("AI classified all %d items as NOT LEAD — returning empty", len(results))
            return []
    except Exception as e:
        logger.warning("AI classification failed, falling back to keyword filter: %s", e)

    if not ai_succeeded:
        strict_keywords = [
            "website", "web design", "web develop", "web dev",
            "build my website", "build a website", "create a website",
            "need web", "looking for web", "new website",
            "landing page", "wordpress",
            "need a website", "my website", "business website",
            "need a designer", "help with my website",
            "redesign", "update my website",
            "looking for", "need a", "need an", "looking to",
            "need someone", "hire a", "want someone",
            "need help with", "would like", "build me",
            "design my", "make me a", "create my",
        ]
        filtered = []
        for r in results:
            t = r['title'].lower()
            if not any(kw in t for kw in strict_keywords):
                continue
            if any(kw in t for kw in ['i build website', 'i offer web', 'i am a web developer',
                                       'affordable web design package', 'web hosting']):
                continue
            filtered.append(r)
        return filtered[:10]
    return []

@app.get("/health")
async def health():
    return {"status": "ok"}

@app.post("/scrape/facebook")
async def scrape_facebook_endpoint(profile_path: str | None = Query(None), force: bool = Query(False)):
    result = await scrape_facebook(profile_path, force)
    return result

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=PORT)
