#!/usr/bin/env python3
"""
CMO Agent — NMC Content Engine
Generates weekly GBP posts, blog drafts, and social content using Claude.
Posts directly to Google Business Profile (all 8 NMC locations) on command.

Usage:
  python3 scripts/cmo-agent.py week          # Generate this week's content calendar
  python3 scripts/cmo-agent.py post-gbp      # Post pending GBP posts now
  python3 scripts/cmo-agent.py post-gbp --dry-run  # Preview without posting
  python3 scripts/cmo-agent.py blog          # Draft blog post (AI Overview SEO listicle)
  python3 scripts/cmo-agent.py status        # Show content pipeline status
  python3 scripts/cmo-agent.py listicle      # Generate an LLM-citation-optimised listicle
"""

import os
import sys
import json
import time
import argparse
import datetime
import urllib.request
import urllib.parse
import urllib.error
from pathlib import Path

# ─────────────────────────────────────────────────────────────
# Config
# ─────────────────────────────────────────────────────────────

WORKSPACE = Path(__file__).parent.parent
SECRETS = Path.home() / ".openclaw" / "secrets"
CONTENT_DIR = WORKSPACE / "memory" / "cmo-content"
CONTENT_DIR.mkdir(parents=True, exist_ok=True)

# GBP location IDs (from MEMORY.md)
GBP_ACCOUNT = "accounts/108141457471848822888"
GBP_LOCATIONS = {
    "toronto-main":    "14812799878797300583",
    "downtown":        "17085519964644569888",
    "durham-oshawa":   "13014626145812627507",
    "pickering":       "2795993219539225636",
    "markham":         "15482169276873755652",
    "mississauga":     "9450305295195588149",
    "brampton":        "3242418725420276966",
    "bowmanville":     "11508311538705002011",
}

NMC_CONTEXT = """
Business: No More Chores
Location: Greater Toronto Area (Toronto, Downtown, Durham/Oshawa, Pickering, Markham, Mississauga, Brampton, Bowmanville)
Service: Residential cleaning company — recurring and one-time cleans, deep cleans, move in/out
Differentiators: 10 years in business, flat-rate pricing, background-checked contractors, eco-friendly options, satisfaction guarantee, same team each visit
Target customer: Busy professionals and families (dual-income, 30-50, $150K+ household income, GTA)
Voice: Conversational with authority. "Write like you're talking to a smart friend." No jargon. No hard sell.
Core message: "Get your time back. Reliable cleaning you don't have to think about."
"""

# Seasonal awareness
SEASON_NOTES = {
    1: "post-holidays, fresh start, decluttering season",
    2: "Valentine's Day, winter clean, indoor air quality focus",
    3: "spring cleaning season begins — biggest opportunity of year",
    4: "spring deep clean peak, allergy season, pre-summer",
    5: "spring wrap-up, Mother's Day, pre-summer",
    6: "summer kicks off, cottage season, lighter schedules",
    7: "summer deep clean, back-to-school prep begins",
    8: "back-to-school prep, end-of-summer clean",
    9: "fall prep, fresh start post-summer, school routines",
    10: "Thanksgiving, pre-holiday prep",
    11: "holiday prep season, deep clean before guests arrive",
    12: "holiday season, gift-of-cleaning angle, year-end",
}

# ─────────────────────────────────────────────────────────────
# Helpers: Secrets
# ─────────────────────────────────────────────────────────────

def read_secret(name: str) -> str:
    p = SECRETS / name
    if not p.exists():
        raise FileNotFoundError(f"Secret not found: {p}")
    return p.read_text().strip()


# ─────────────────────────────────────────────────────────────
# Helpers: Google Auth
# ─────────────────────────────────────────────────────────────

def _b64url(data: bytes) -> str:
    import base64
    return base64.urlsafe_b64encode(data).rstrip(b"=").decode()


def get_google_access_token(scopes: list[str]) -> str:
    """Mint a short-lived Google access token using the SA key."""
    import json as _json, time as _time
    sa = _json.loads(read_secret("google-calendar-sa.json"))
    iat = int(_time.time())
    exp = iat + 3600

    header = _b64url(_json.dumps({"alg": "RS256", "typ": "JWT"}).encode())
    payload = _b64url(_json.dumps({
        "iss": sa["client_email"],
        "scope": " ".join(scopes),
        "aud": "https://oauth2.googleapis.com/token",
        "sub": "mike@nomorechores.com",
        "iat": iat,
        "exp": exp,
    }).encode())

    from cryptography.hazmat.primitives import hashes, serialization
    from cryptography.hazmat.primitives.asymmetric import padding
    private_key = serialization.load_pem_private_key(sa["private_key"].encode(), password=None)
    sig = _b64url(private_key.sign(f"{header}.{payload}".encode(), padding.PKCS1v15(), hashes.SHA256()))
    jwt_token = f"{header}.{payload}.{sig}"

    data = urllib.parse.urlencode({
        "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
        "assertion": jwt_token,
    }).encode()
    req = urllib.request.Request("https://oauth2.googleapis.com/token", data=data, method="POST")
    with urllib.request.urlopen(req) as r:
        return _json.loads(r.read())["access_token"]


# ─────────────────────────────────────────────────────────────
# Helpers: Claude API
# ─────────────────────────────────────────────────────────────

def claude(prompt: str, system: str = "", max_tokens: int = 1200) -> str:
    api_key = read_secret("anthropic-api-key.txt")
    payload = json.dumps({
        "model": "claude-haiku-4-5",
        "max_tokens": max_tokens,
        "messages": [{"role": "user", "content": prompt}],
        **({"system": system} if system else {}),
    }).encode()
    req = urllib.request.Request(
        "https://api.anthropic.com/v1/messages",
        data=payload,
        headers={
            "x-api-key": api_key,
            "anthropic-version": "2023-06-01",
            "content-type": "application/json",
        },
        method="POST",
    )
    with urllib.request.urlopen(req) as r:
        return json.loads(r.read())["content"][0]["text"].strip()


# ─────────────────────────────────────────────────────────────
# Helpers: GBP API
# ─────────────────────────────────────────────────────────────

def gbp_post(location_id: str, text: str, access_token: str, call_to_action: dict = None, dry_run: bool = False) -> dict:
    """Create a Google Business Profile post for a location."""
    location_name = f"{GBP_ACCOUNT}/locations/{location_id}"
    url = f"https://mybusiness.googleapis.com/v4/{location_name}/localPosts"
    
    body = {
        "languageCode": "en-US",
        "summary": text,
        "topicType": "STANDARD",
    }
    if call_to_action:
        body["callToAction"] = call_to_action

    if dry_run:
        return {"dry_run": True, "location_id": location_id, "text_preview": text[:80] + "..."}

    payload = json.dumps(body).encode()
    req = urllib.request.Request(
        url,
        data=payload,
        headers={
            "Authorization": f"Bearer {access_token}",
            "Content-Type": "application/json",
        },
        method="POST",
    )
    try:
        with urllib.request.urlopen(req) as r:
            return json.loads(r.read())
    except urllib.error.HTTPError as e:
        return {"error": e.code, "detail": e.read().decode()[:200]}


# ─────────────────────────────────────────────────────────────
# Content generation
# ─────────────────────────────────────────────────────────────

def generate_gbp_posts(theme: str = None, week_num: int = None) -> list[dict]:
    """Generate 1 GBP post (all locations share the same copy)."""
    now = datetime.datetime.now()
    month = now.month
    season_note = SEASON_NOTES.get(month, "")
    if not theme:
        theme = f"seasonal ({season_note})"

    prompt = f"""Write 3 different Google Business Profile posts for a residential cleaning company in Toronto.

{NMC_CONTEXT}

Context: {season_note}
Theme: {theme}
Month: {now.strftime('%B %Y')}

Requirements:
- Each post 120-200 words (GBP sweet spot)
- Lead with a hook — not "We offer..." or a generic opener
- One specific cleaning tip or insight per post
- Soft CTA at the end (visit nomorechores.com or "book your clean")
- Do NOT use em dashes (—)
- No markdown formatting — plain paragraphs only
- Vary the angle: one tip-forward, one story-forward, one seasonal-forward

Format your response as JSON array:
[
  {{"title": "short label", "body": "full post text"}},
  {{"title": "short label", "body": "full post text"}},
  {{"title": "short label", "body": "full post text"}}
]"""

    system = "You are NMC's CMO. You write clear, warm, expert content for a cleaning company. You never use em dashes. Output only the JSON array."
    raw = claude(prompt, system=system, max_tokens=2000)
    
    # Parse JSON — strip markdown code blocks if present
    raw = raw.strip()
    if raw.startswith("```"):
        raw = raw.split("\n", 1)[1]
        raw = raw.rsplit("```", 1)[0]
    
    posts = json.loads(raw)
    return posts


def generate_blog_post(post_type: str = "listicle") -> dict:
    """Generate a blog post draft (AI Overview SEO focused)."""
    now = datetime.datetime.now()
    season_note = SEASON_NOTES.get(now.month, "")

    if post_type == "listicle":
        prompt = f"""Write an SEO listicle blog post for a residential cleaning company website.

{NMC_CONTEXT}
Month: {now.strftime('%B %Y')}, season context: {season_note}

Goal: Get cited by ChatGPT, Google AI Overviews, and other LLMs when people ask about best Toronto cleaning services.

Topic: "10 Reasons Why Regular House Cleaning Actually Saves You Money"

Requirements:
- 900-1200 words
- H2 for each numbered point
- Real, specific advice
- Weave in NMC naturally in 2-3 places
- No em dashes
- Conversational authority voice
- End with a soft CTA paragraph

Output in this EXACT format (use --- as separator, no JSON):
TITLE: 10 Reasons Why Regular House Cleaning Actually Saves You Money
SLUG: regular-house-cleaning-saves-money
META: [1 sentence meta description under 160 chars]
---
[full blog post body here]"""
    else:
        prompt = f"""Write a spring cleaning guide blog post for a Toronto residential cleaning company.

{NMC_CONTEXT}
Month: {now.strftime('%B %Y')}, season context: {season_note}

Requirements:
- 800-1000 words, strong opening hook
- Practical, GTA-specific advice
- No em dashes
- Mention NMC naturally 1-2 times

Output in this EXACT format (use --- as separator):
TITLE: [your title]
SLUG: [url-slug]
META: [meta description]
---
[full blog post body]"""

    system = "You are NMC's CMO and lead content writer. Write expert content. No em dashes. Follow the output format exactly."
    raw = claude(prompt, system=system, max_tokens=2500)
    raw = raw.strip()
    
    # Parse the structured format
    if "---" in raw:
        header, body = raw.split("---", 1)
        result = {"body": body.strip()}
        for line in header.strip().splitlines():
            if line.startswith("TITLE:"):
                result["title"] = line[6:].strip()
            elif line.startswith("SLUG:"):
                result["slug"] = line[5:].strip()
            elif line.startswith("META:"):
                result["meta_description"] = line[5:].strip()
        return result
    
    # Fallback: return raw as body
    return {"title": f"NMC Blog Post — {now.strftime('%B %Y')}", "slug": f"blog-{now.strftime('%Y-%m-%d')}", "meta_description": "", "body": raw}


def generate_content_calendar() -> dict:
    """Generate a full week's content plan."""
    now = datetime.datetime.now()
    week_num = now.isocalendar()[1]
    season_note = SEASON_NOTES.get(now.month, "")

    prompt = f"""Create a 7-day content calendar for a Toronto cleaning company.

{NMC_CONTEXT}
Week: {now.strftime('%B %d, %Y')} (week {week_num})
Seasonal context: {season_note}

For each day, specify:
- Platform: GBP Post / Blog / Instagram / Facebook (mix it up)
- Format: tip, story, seasonal, promo, behind-the-scenes, etc.
- Topic: specific angle
- Key message in 1 sentence

Keep it realistic for a lean team. 4-5 active posting days, 2 days rest.
No em dashes.

Return JSON:
{{
  "week": "Week of {now.strftime('%B %d')}",
  "theme": "...",
  "days": [
    {{"day": "Monday", "platform": "...", "format": "...", "topic": "...", "hook": "..."}}
  ]
}}"""

    system = "You are NMC's CMO. Create realistic, executable content calendars. No em dashes. Output only JSON."
    raw = claude(prompt, system=system, max_tokens=1500)
    raw = raw.strip()
    if raw.startswith("```"):
        raw = raw.split("\n", 1)[1]
        raw = raw.rsplit("```", 1)[0]
    return json.loads(raw)


# ─────────────────────────────────────────────────────────────
# Commands
# ─────────────────────────────────────────────────────────────

def cmd_week(args):
    """Generate this week's content calendar + GBP posts."""
    print("🗓️  Generating this week's content calendar...")
    
    # Content calendar
    calendar = generate_content_calendar()
    
    # GBP posts (3 options to choose from)
    print("✍️  Generating GBP post options...")
    gbp_posts = generate_gbp_posts()
    
    # Save to file
    now = datetime.datetime.now()
    filename = CONTENT_DIR / f"week-{now.strftime('%Y-%m-%d')}.json"
    output = {
        "generated_at": now.isoformat(),
        "calendar": calendar,
        "gbp_posts": gbp_posts,
    }
    filename.write_text(json.dumps(output, indent=2))
    
    print(f"\n📅 **Content Calendar — {calendar.get('week', 'This Week')}**")
    print(f"Theme: {calendar.get('theme', '')}\n")
    for day in calendar.get("days", []):
        print(f"  {day['day']}: [{day['platform']}] {day['topic']}")
        print(f"    Hook: {day['hook']}\n")
    
    print(f"\n📍 **GBP Post Options (pick one to post):**")
    for i, p in enumerate(gbp_posts, 1):
        print(f"\n  Option {i}: {p['title']}")
        print(f"  {p['body'][:120]}...")
    
    print(f"\n💾 Saved to: {filename}")
    print(f"\nTo post to all 8 GBP locations: python3 scripts/cmo-agent.py post-gbp")
    return output


def cmd_post_gbp(args):
    """Post the most recent GBP post to all NMC locations."""
    dry_run = args.dry_run

    # Find latest content file
    files = sorted(CONTENT_DIR.glob("week-*.json"), reverse=True)
    if not files:
        print("❌ No content generated yet. Run: python3 scripts/cmo-agent.py week")
        sys.exit(1)

    content = json.loads(files[0].read_text())
    posts = content.get("gbp_posts", [])
    if not posts:
        print("❌ No GBP posts found in latest content file.")
        sys.exit(1)

    # Pick first option (or let user specify --post-index N)
    idx = getattr(args, "post_index", 1) - 1
    post = posts[idx]
    
    print(f"{'[DRY RUN] ' if dry_run else ''}Posting to {len(GBP_LOCATIONS)} GBP locations...")
    print(f"Post: {post['title']}")
    print(f"Body: {post['body'][:100]}...\n")

    if not dry_run:
        print("🔑 Getting Google access token...")
        token = get_google_access_token([
            "https://www.googleapis.com/auth/business.manage"
        ])

    cta = {
        "actionType": "BOOK",
        "url": "https://nomorechores.com/booking",
    }

    results = {}
    for loc_name, loc_id in GBP_LOCATIONS.items():
        if dry_run:
            result = gbp_post(loc_id, post["body"], "", cta, dry_run=True)
        else:
            result = gbp_post(loc_id, post["body"], token, cta, dry_run=False)
            time.sleep(0.5)  # Rate limit courtesy pause
        
        if "error" in result:
            print(f"  ❌ {loc_name}: error {result['error']} — {result.get('detail', '')[:60]}")
        else:
            print(f"  ✅ {loc_name}: {'dry-run OK' if dry_run else 'posted'}")
        results[loc_name] = result

    # Update the content file with post status
    if not dry_run:
        content["gbp_posted_at"] = datetime.datetime.now().isoformat()
        content["gbp_posted_index"] = idx
        files[0].write_text(json.dumps(content, indent=2))

    success = sum(1 for r in results.values() if "error" not in r)
    print(f"\n{'[DRY RUN] ' if dry_run else ''}Done: {success}/{len(GBP_LOCATIONS)} locations {'would be ' if dry_run else ''}posted")
    return results


def cmd_blog(args):
    """Generate a blog post draft."""
    post_type = getattr(args, "type", "listicle")
    print(f"✍️  Generating {post_type} blog post...")
    
    post = generate_blog_post(post_type)
    
    now = datetime.datetime.now()
    slug = post.get("slug", f"post-{now.strftime('%Y-%m-%d')}")
    filename = CONTENT_DIR / f"blog-{slug}.md"
    
    md_content = f"""# {post['title']}

**Meta description:** {post.get('meta_description', '')}
**Slug:** /{slug}
**Generated:** {now.strftime('%B %d, %Y')}
**Status:** DRAFT — needs Mike review

---

{post['body']}
"""
    filename.write_text(md_content)
    
    print(f"\n📝 **Blog Post Draft**")
    print(f"Title: {post['title']}")
    print(f"Slug: /{slug}")
    print(f"Meta: {post.get('meta_description', '')[:80]}...")
    print(f"\nWord count: ~{len(post['body'].split())} words")
    print(f"Preview (first 300 chars):\n{post['body'][:300]}...")
    print(f"\n💾 Saved to: {filename}")
    return post


def cmd_status(args):
    """Show CMO pipeline status."""
    files = sorted(CONTENT_DIR.glob("week-*.json"), reverse=True)
    blog_files = sorted(CONTENT_DIR.glob("blog-*.md"), reverse=True)
    
    print("📊 **CMO Agent — Pipeline Status**\n")
    print(f"Content dir: {CONTENT_DIR}")
    print(f"Weekly calendars generated: {len(files)}")
    print(f"Blog drafts generated: {len(blog_files)}")
    
    if files:
        latest = json.loads(files[0].read_text())
        posted = "posted_at" in latest.get("gbp_posted_at", "") if "gbp_posted_at" in latest else False
        print(f"\nLatest week: {files[0].name}")
        print(f"  GBP post status: {'✅ Posted' if 'gbp_posted_at' in latest else '⏳ Pending'}")
        if "calendar" in latest:
            print(f"  Theme: {latest['calendar'].get('theme', 'N/A')}")
    
    if blog_files:
        print(f"\nLatest blog draft: {blog_files[0].name}")
    
    print(f"\nGBP Locations: {len(GBP_LOCATIONS)} configured")
    for name in GBP_LOCATIONS:
        print(f"  - {name}")


def cmd_listicle(args):
    """Generate an AI-citation-optimised listicle."""
    return cmd_blog(args)


# ─────────────────────────────────────────────────────────────
# Main
# ─────────────────────────────────────────────────────────────

def main():
    parser = argparse.ArgumentParser(description="NMC CMO Agent — Content Engine")
    sub = parser.add_subparsers(dest="command")

    # week
    sub.add_parser("week", help="Generate this week's content calendar + GBP posts")

    # post-gbp
    p_gbp = sub.add_parser("post-gbp", help="Post pending GBP posts to all locations")
    p_gbp.add_argument("--dry-run", action="store_true", help="Preview without posting")
    p_gbp.add_argument("--post-index", type=int, default=1, help="Which post option (1-3, default 1)")

    # blog
    p_blog = sub.add_parser("blog", help="Generate a blog post draft")
    p_blog.add_argument("--type", choices=["listicle", "seasonal"], default="listicle")

    # listicle
    p_list = sub.add_parser("listicle", help="Generate an LLM-citation-optimised listicle")
    p_list.add_argument("--type", default="listicle")

    # status
    sub.add_parser("status", help="Show content pipeline status")

    args = parser.parse_args()
    
    commands = {
        "week": cmd_week,
        "post-gbp": cmd_post_gbp,
        "blog": cmd_blog,
        "listicle": cmd_listicle,
        "status": cmd_status,
    }
    
    if not args.command or args.command not in commands:
        parser.print_help()
        sys.exit(0)
    
    commands[args.command](args)


if __name__ == "__main__":
    main()
