#!/usr/bin/env python3
"""
WeChat Official Account Publishing Script
Publishes HTML article to WeChat Official Account via API.

Supports:
1. Upload images to WeChat material library
2. Create draft article
3. Publish draft article

Requires: WeChat AppID and AppSecret configured in environment or config file.

Usage:
    python publish_to_wechat.py article.html --title "文章标题" --author "抟微科技"
    python publish_to_wechat.py article.html --title "标题" --cover cover.png --dry-run
    python publish_to_wechat.py article.html --config wechat_config.json
"""

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


# ============================================================
# Config Loading
# ============================================================

CONFIG_PATHS = [
    Path.cwd() / ".wechat" / "config.json",
    Path.home() / ".wechat" / "config.json",
    Path.cwd() / "wechat_config.json",
]

ENV_PATHS = [
    Path.cwd() / ".env",
    Path.cwd() / ".wechat" / ".env",
    Path.home() / ".wechat" / ".env",
    Path.home() / ".baoyu-skills" / ".env",
]
ACCOUNT_ID_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,47}$")


def account_paths(account_id):
    """Return local account paths without ever accepting a path-like id."""
    if not ACCOUNT_ID_RE.fullmatch(account_id or ""):
        raise ValueError("Invalid account id. Use lowercase letters, digits, and hyphens only.")
    root = Path.home() / ".wechat" / "accounts"
    return (
        [root / account_id / "credentials.env", root / account_id / ".env", root / f"{account_id}.env"],
        [root / account_id / "config.json", root / f"{account_id}.json"],
    )


def load_dotenv(path, override=False):
    """Load simple KEY=VALUE entries and return the values read."""
    values = {}
    if not path.exists():
        return values

    for raw_line in path.read_text(encoding="utf-8").splitlines():
        line = raw_line.strip()
        if not line or line.startswith("#"):
            continue
        if line.startswith("export "):
            line = line[7:].lstrip()
        if "=" not in line:
            continue

        key, value = line.split("=", 1)
        key = key.strip()
        value = value.strip()
        if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
            value = value[1:-1]
        if not key:
            continue
        values[key] = value
        if override or key not in os.environ:
            os.environ[key] = value
    return values


def load_config(config_path=None, env_path=None, account_id=None):
    """Load WeChat config from file or environment."""
    config = {}

    # Load a local dotenv file first; real process variables still win.
    if account_id:
        account_env_paths, account_config_paths = account_paths(account_id)
        env_paths = [Path(env_path)] if env_path else account_env_paths
        paths = [Path(config_path)] if config_path else account_config_paths
    else:
        env_paths = [Path(env_path)] if env_path else ENV_PATHS
        paths = [Path(config_path)] if config_path else CONFIG_PATHS
    dotenv_values = {}
    for p in env_paths:
        if p.exists():
            dotenv_values = load_dotenv(p, override=bool(account_id))
            break

    # Try config file
    for p in paths:
        if p.exists():
            with open(p, "r", encoding="utf-8") as f:
                config = json.load(f)
            break

    # Process variables override legacy/default loading. An explicit account uses
    # only its local credential file to avoid cross-account leakage.
    if account_id:
        if dotenv_values.get("WECHAT_APP_ID"):
            config["app_id"] = dotenv_values["WECHAT_APP_ID"]
        if dotenv_values.get("WECHAT_APP_SECRET"):
            config["app_secret"] = dotenv_values["WECHAT_APP_SECRET"]
    else:
        if os.environ.get("WECHAT_APP_ID"):
            config["app_id"] = os.environ["WECHAT_APP_ID"]
        if os.environ.get("WECHAT_APP_SECRET"):
            config["app_secret"] = os.environ["WECHAT_APP_SECRET"]

    if not config.get("app_id") or not config.get("app_secret"):
        print(
            "Error: WeChat credentials not found.\n"
            "Please configure via one of:\n"
            "  1. Environment variables: WECHAT_APP_ID, WECHAT_APP_SECRET\n"
            "  2. .env file: ./.env, ./.wechat/.env, or ~/.wechat/.env\n"
            "  3. Config file: ~/.wechat/config.json or ./.wechat/config.json\n"
            "  4. --config path/to/config.json\n"
            "  5. --account tiny-wings (local ~/.wechat/accounts/...)\n\n"
            "Config file format:\n"
            '{\n  "app_id": "wx1234567890",\n  "app_secret": "your_secret"\n}',
            file=sys.stderr
        )
        sys.exit(1)

    return config


# ============================================================
# WeChat API Client
# ============================================================

class WeChatClient:
    """Minimal WeChat Official Account API client."""

    BASE_URL = "https://api.weixin.qq.com/cgi-bin"

    def __init__(self, app_id, app_secret):
        self.app_id = app_id
        self.app_secret = app_secret
        self._access_token = None
        self._token_expires = 0

    def _get_access_token(self):
        """Get or refresh access token."""
        if self._access_token and time.time() < self._token_expires - 300:
            return self._access_token

        url = f"{self.BASE_URL}/token"
        params = {
            "grant_type": "client_credential",
            "appid": self.app_id,
            "secret": self.app_secret,
        }
        url = f"{url}?{urllib.parse.urlencode(params)}"

        try:
            resp = self._http_get(url)
            if "access_token" in resp:
                self._access_token = resp["access_token"]
                self._token_expires = time.time() + resp.get("expires_in", 7200)
                return self._access_token
            else:
                raise Exception(f"Failed to get access token: {resp}")
        except Exception as e:
            raise Exception(f"Failed to get access token: {e}\n"
                          "Please check:\n"
                          "  1. AppID and AppSecret are correct\n"
                          "  2. Your server IP is in the WeChat IP whitelist\n"
                          "  3. Your account has the necessary API permissions")

    def _http_request(self, url, method="GET", data=None, is_json=True):
        """Make HTTP request to WeChat API."""
        if data and is_json:
            data = json.dumps(data, ensure_ascii=False).encode("utf-8")
        elif data and not is_json:
            if isinstance(data, str):
                data = data.encode("utf-8")

        req = urllib.request.Request(url, data=data, method=method)
        if is_json and data:
            req.add_header("Content-Type", "application/json; charset=utf-8")

        try:
            with urllib.request.urlopen(req, timeout=30) as resp:
                body = resp.read().decode("utf-8")
                return json.loads(body) if body else {}
        except urllib.error.HTTPError as e:
            error_body = e.read().decode("utf-8") if e.fp else ""
            raise Exception(f"HTTP {e.code}: {error_body}")
        except urllib.error.URLError as e:
            raise Exception(f"URL Error: {e.reason}")

    def _http_get(self, url):
        return self._http_request(url, method="GET")

    def _http_post(self, url, data, is_json=True):
        return self._http_request(url, method="POST", data=data, is_json=is_json)

    def upload_image(self, image_path):
        """Upload image to WeChat material library. Returns media_id."""
        token = self._get_access_token()
        url = f"{self.BASE_URL}/media/upload?access_token={token}&type=image"

        image_path = Path(image_path)
        if not image_path.exists():
            raise FileNotFoundError(f"Image not found: {image_path}")

        # Multipart form data
        boundary = "----WebKitFormBoundary" + hashlib.md5(str(time.time()).encode()).hexdigest()
        with open(image_path, "rb") as f:
            file_data = f.read()

        body = (
            f"--{boundary}\r\n"
            f'Content-Disposition: form-data; name="media"; '
            f'filename="{image_path.name}"; '
            f'Content-Type: image/{image_path.suffix[1:]}\r\n\r\n'
        ).encode("utf-8") + file_data + f"\r\n--{boundary}--\r\n".encode("utf-8")

        req = urllib.request.Request(url, data=body, method="POST")
        req.add_header("Content-Type", f"multipart/form-data; boundary={boundary}")

        try:
            with urllib.request.urlopen(req, timeout=60) as resp:
                result = json.loads(resp.read().decode("utf-8"))
                if "media_id" in result:
                    print(f"  Image uploaded: {image_path.name} -> {result['media_id']}")
                    return result["media_id"]
                else:
                    raise Exception(f"Upload failed: {result}")
        except Exception as e:
            raise Exception(f"Image upload failed: {e}")

    def upload_thumb(self, thumb_path):
        """Upload permanent thumbnail image. Returns media_id."""
        token = self._get_access_token()
        url = f"{self.BASE_URL}/material/add_material?access_token={token}&type=image"

        thumb_path = Path(thumb_path)
        with open(thumb_path, "rb") as f:
            file_data = f.read()

        boundary = "----WebKitFormBoundary" + hashlib.md5(str(time.time()).encode()).hexdigest()
        body = (
            f"--{boundary}\r\n"
            f'Content-Disposition: form-data; name="media"; '
            f'filename="{thumb_path.name}"; '
            f'Content-Type: image/{thumb_path.suffix[1:]}\r\n\r\n'
        ).encode("utf-8") + file_data + f"\r\n--{boundary}--\r\n".encode("utf-8")

        req = urllib.request.Request(url, data=body, method="POST")
        req.add_header("Content-Type", f"multipart/form-data; boundary={boundary}")

        with urllib.request.urlopen(req, timeout=60) as resp:
            result = json.loads(resp.read().decode("utf-8"))
            if "media_id" in result:
                return result["media_id"]
            raise Exception(f"Thumb upload failed: {result}")

    def add_draft(self, title, content, thumb_media_id=None, author="抟微科技",
                  digest=None, need_open_comment=1, only_fans_can_comment=0):
        """Create a draft article. Returns media_id of the draft."""
        token = self._get_access_token()
        url = f"{self.BASE_URL}/draft/add?access_token={token}"

        article = {
            "title": title,
            "author": author,
            "content": content,
            "content_source_url": "",
            "need_open_comment": need_open_comment,
            "only_fans_can_comment": only_fans_can_comment,
        }

        if thumb_media_id:
            article["thumb_media_id"] = thumb_media_id
        if digest:
            article["digest"] = digest

        data = {"articles": [article]}
        result = self._http_post(url, data)

        if "media_id" in result:
            print(f"  Draft created: {result['media_id']}")
            return result["media_id"]
        else:
            raise Exception(f"Draft creation failed: {result}")

    def publish(self, media_id):
        """Publish a draft article. Returns publish_id."""
        token = self._get_access_token()
        url = f"{self.BASE_URL}/freepublish/submit?access_token={token}"
        data = {"media_id": media_id}
        result = self._http_post(url, data)

        if "publish_id" in result:
            print(f"  Published: {result['publish_id']}")
            return result["publish_id"]
        else:
            raise Exception(f"Publish failed: {result}")

    def get_draft_count(self):
        """Get total draft count."""
        token = self._get_access_token()
        url = f"{self.BASE_URL}/draft/count?access_token={token}"
        return self._http_get(url)

    def get_draft_list(self, offset=0, count=20):
        """List draft articles."""
        token = self._get_access_token()
        url = f"{self.BASE_URL}/draft/batchget?access_token={token}"
        data = {"offset": offset, "count": count, "no_content": 1}
        return self._http_post(url, data)


# ============================================================
# Image Processing in HTML
# ============================================================

def process_images_in_html(html_content, client, base_dir=None):
    """Find image URLs in HTML, upload local images to WeChat, replace URLs."""
    import re

    def replace_image(match):
        full_match = match.group(0)
        src = match.group(1)

        # Skip already-uploaded WeChat URLs
        if "mmbiz.qpic.cn" in src:
            return full_match

        # Handle local images
        if src.startswith("http://") or src.startswith("https://"):
            return full_match  # Skip remote URLs (WeChat will proxy them)

        # Local file
        if base_dir:
            img_path = Path(base_dir) / src
        else:
            img_path = Path(src)

        if img_path.exists():
            try:
                media_id = client.upload_image(str(img_path))
                # WeChat image URL format
                wechat_url = f"https://mmbiz.qpic.cn/mmbiz/{media_id}/0"
                return full_match.replace(src, wechat_url)
            except Exception as e:
                print(f"  Warning: Failed to upload image {src}: {e}", file=sys.stderr)
                return full_match
        else:
            print(f"  Warning: Image not found: {img_path}", file=sys.stderr)
            return full_match

    # Match img src attributes
    pattern = r'<img[^>]+src=["\']([^"\']+)["\']'
    return re.sub(pattern, replace_image, html_content)


# ============================================================
# Main CLI
# ============================================================

def main():
    parser = argparse.ArgumentParser(
        description="Publish HTML article to WeChat Official Account"
    )
    parser.add_argument("input", help="Input HTML file path")
    parser.add_argument("--title", required=True, help="Article title")
    parser.add_argument("--author", default="抟微科技", help="Author name")
    parser.add_argument("--cover", help="Cover image path (recommended 900x383 or 2.35:1)")
    parser.add_argument("--digest", help="Article digest/summary (optional, auto-generated if omitted)")
    parser.add_argument("--config", help="Config file path")
    parser.add_argument("--env-file", help="dotenv file path (optional)")
    parser.add_argument(
        "--account",
        help="Local account id, for example tiny-wings or sanhe-xu. Secrets stay in ~/.wechat/accounts/.",
    )
    parser.add_argument("--dry-run", action="store_true",
                        help="Validate without publishing (check config, format, images)")
    parser.add_argument("--publish", action="store_true",
                        help="Actually publish (default: create draft only)")
    parser.add_argument("--no-comment", action="store_true",
                        help="Disable comments")
    parser.add_argument("--fans-only", action="store_true",
                        help="Only fans can comment")

    args = parser.parse_args()

    # Read HTML
    input_path = Path(args.input)
    if not input_path.exists():
        print(f"Error: File not found: {args.input}", file=sys.stderr)
        sys.exit(1)

    html_content = input_path.read_text(encoding="utf-8")

    # Dry run mode
    if args.dry_run:
        print("=== Dry Run Mode ===")
        print(f"Title: {args.title}")
        print(f"Author: {args.author}")
        print(f"Content length: {len(html_content)} chars")

        # Count images
        import re
        images = re.findall(r'<img[^>]+src=["\']([^"\']+)["\']', html_content)
        print(f"Images found: {len(images)}")
        for i, img in enumerate(images):
            print(f"  {i+1}. {img}")

        if args.cover:
            print(f"Cover image: {args.cover}")

        print("\nDry run complete. No changes made.")
        return

    # Load config and create client
    config = load_config(args.config, args.env_file, args.account)
    client = WeChatClient(config["app_id"], config["app_secret"])

    print("=== WeChat Publishing ===")
    if args.account:
        print(f"Account: {args.account}")
    print(f"Title: {args.title}")
    print(f"Author: {args.author}")

    # Process images in HTML
    print("\n[1/4] Processing images...")
    base_dir = input_path.parent
    html_content = process_images_in_html(html_content, client, base_dir)

    # Upload cover image
    thumb_media_id = None
    if args.cover:
        print("\n[2/4] Uploading cover image...")
        try:
            thumb_media_id = client.upload_thumb(args.cover)
            print(f"  Cover uploaded: {thumb_media_id}")
        except Exception as e:
            print(f"  Warning: Cover upload failed: {e}", file=sys.stderr)
            print("  Continuing without cover...")

    # Create draft
    print("\n[3/4] Creating draft...")
    need_open_comment = 0 if args.no_comment else 1
    only_fans_can_comment = 1 if args.fans_only else 0

    media_id = client.add_draft(
        title=args.title,
        content=html_content,
        thumb_media_id=thumb_media_id,
        author=args.author,
        digest=args.digest,
        need_open_comment=need_open_comment,
        only_fans_can_comment=only_fans_can_comment,
    )

    print(f"\n  Draft media_id: {media_id}")

    # Publish if requested
    if args.publish:
        print("\n[4/4] Publishing...")
        publish_id = client.publish(media_id)
        print(f"\n=== Published! ===")
        print(f"  publish_id: {publish_id}")
    else:
        print(f"\n=== Draft Created! ===")
        print(f"  Review in WeChat Official Account backend")
        print(f"  Use --publish flag to publish directly")

    return media_id


if __name__ == "__main__":
    main()
