#!/usr/bin/env python3
"""
sendmsg — Universal message sender for Signal and SMS/iMessage.

Usage:
  sendmsg --signal --to +18005551212 --text "Hello from Signal!"
  sendmsg --signal --recipients group_id_here --text "All members"
  sendmsg --sms   --to +18005551212 --text "Hello via SMS!"
  sendmsg --sms   --to +18005551212 --text "Hey" --file ~/photo.png
  sendmsg --signal --to +18005551212 --text "Multi" --text "more text"
  sendmsg --signal --to +18005551212 --attach ~/doc.pdf ~/pic.jpg
  sendmsg --csv messages.csv                  # Bulk send from CSV file
  sendmsg --csv messages.csv --delay 2        # Wait N seconds between sends
  sendmsg --list-signal       # Show linked Signal accounts
  sendmsg --link-signal        # Link a new device to your existing Signal account

CSV format (messages.csv):
  method,recipient,name,message,account,service,file,delay
  signal,+1888222333,Alice,Hello via Signal,+1888222333,,,
  signal,group.ZzBHd3NZO...,Group alert,,,,-1
  sms,+123****7890,Bob,SMS test,,,~/pic.jpg,
  signal,+1888222333,Carol,With delay,,,-3
"""

import argparse
import configparser
import csv
import json
import os
import subprocess
import sys
import time
import urllib.error
import urllib.parse
import urllib.request

VERSION = "4.0.0"
CONFIG_PATHS = [
    os.path.expanduser("~/.sendmsg.conf"),
]


def load_config():
    """Load settings from sendmsg config file, with env var fallback.

    Config file format (INI-style):
        [settings]
        signal_rest_url = http://localhost:8080
        signal_default_account = +1234567890

    Lookup order: env var > config file > hardcoded default.
    """
    defaults = {
        "signal_rest_url": "http://localhost:8080",
        "signal_default_account": "+1234567890",
    }
    config = configparser.ConfigParser()

    for path in CONFIG_PATHS:
        if os.path.isfile(path):
            config.read(path)
            break

    return {
        "signal_rest_url": (
            os.environ.get("SIGNAL_REST_URL")
            or config.get("settings", "signal_rest_url", fallback=defaults["signal_rest_url"])
        ),
        "signal_default_account": (
            os.environ.get("SIGNAL_ACCOUNT")
            or config.get("settings", "signal_default_account", fallback=defaults["signal_default_account"])
        ),
    }


# Load config at module level for use by all functions
CFG = load_config()
SIGNAL_REST_URL = CFG["signal_rest_url"]
SIGNAL_DEFAULT_ACCOUNT = CFG["signal_default_account"]


# ---------------------------------------------------------------------------
# Helpers — reusable without argparse objects (needed for CSV dispatch)
# ---------------------------------------------------------------------------

def send_one_signal(account, recipient, message, group_id=None, attach=None):
    """Send a single Signal message. Returns True on success."""
    payload = {"number": account}

    if group_id:
        payload["recipients"] = [group_id]
    else:
        payload["recipient"] = recipient

    if message:
        payload["message"] = message

    # Attachments
    if attach and os.path.isfile(attach):
        result = signal_rest_post("/v2/send", payload=payload, files=[attach])
    elif message and not group_id:
        result = signal_rest_post("/v2/send", payload=payload)
    else:
        result = signal_rest_post("/v2/send", payload=payload)

    return result is not None


def send_one_sms(to, message, service=None, file=None):
    """Send a single SMS/iMessage. Returns True on success."""
    cmd = ["imsg", "send", "--to", to]

    if service:
        cmd.extend(["--service", service])

    cmd.extend(["--text", message])

    if file and os.path.isfile(file):
        cmd.extend(["--file", file])

    result = subprocess.run(cmd, capture_output=True, text=True)

    if result.returncode == 0:
        service_label = service.upper() if service else "AUTO"
        print(f"✅ SMS/iMessage sent to {to} (service: {service_label})")
        return True
    else:
        print(f"❌ SMS send failed (exit {result.returncode}): {result.stderr.strip()}", file=sys.stderr)
        return False


# ---------------------------------------------------------------------------
# REST API
# ---------------------------------------------------------------------------

def signal_rest_post(endpoint, payload=None, files=None):
    """Post JSON or multipart form data to the signal-cli-rest-api.

    Returns the parsed JSON response dict, or None on failure.
    """
    url = f"{SIGNAL_REST_URL.rstrip('/')}{endpoint}"

    if files is not None and files:
        # Multipart form data for attachments
        boundary = "----sendmsg-boundary"
        body_parts = []

        if payload:
            for key, value in payload.items():
                body_parts.append(
                    f"--{boundary}\r\n"
                    f'Content-Disposition: form-data; name="{key}"\r\n\r\n'
                    f"{value}\r\n"
                )

        for fpath in files:
            if not os.path.isfile(fpath):
                print(f"⚠️  Warning: attachment not found: {fpath}", file=sys.stderr)
                continue
            with open(fpath, "rb") as fh:
                content = fh.read()
            fname = os.path.basename(fpath)
            body_parts.append(
                f"--{boundary}\r\n"
                f"Content-Disposition: form-data; name=\"attachment\"; filename=\"{fname}\"\r\n"
                f"Content-Type: application/octet-stream\r\n\r\n"
            )
            body_parts.append(content)
            body_parts.append(b"\r\n")

        body_parts.append(f"--{boundary}--\r\n".encode("utf-8"))
        full_body = b"".join(body_parts)

        headers = {
            "Content-Type": f"multipart/form-data; boundary={boundary}",
        }
    else:
        # JSON payload
        body = json.dumps(payload).encode("utf-8") if payload else b"{}"
        headers = {"Content-Type": "application/json"}
        full_body = body

    req = urllib.request.Request(url, data=full_body, headers=headers, method="POST")

    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            resp_data = resp.read().decode("utf-8")
            if resp_data.strip():
                result = json.loads(resp_data)
                if isinstance(result, dict) and "error" in result:
                    print(
                        f"❌ Signal REST error: {result['error'].strip()}",
                        file=sys.stderr,
                    )
                    return None
                return result
            return None
    except urllib.error.HTTPError as e:
        body = e.read().decode("utf-8", errors="replace")
        print(
            f"❌ Signal REST error: HTTP {e.code} — {body[:500]}",
            file=sys.stderr,
        )
        return None
    except urllib.error.URLError as e:
        print(f"❌ Cannot reach Signal REST API at {SIGNAL_REST_URL}: {e.reason}", file=sys.stderr)
        return None


def signal_rest_get(endpoint):
    """GET a JSON response from the signal-cli-rest-api."""
    url = f"{SIGNAL_REST_URL.rstrip('/')}{endpoint}"
    req = urllib.request.Request(url, method="GET")
    try:
        with urllib.request.urlopen(req, timeout=15) as resp:
            data = resp.read().decode("utf-8")
            if data.strip():
                return json.loads(data)
            return None
    except urllib.error.HTTPError as e:
        body = e.read().decode("utf-8", errors="replace")
        print(f"❌ Signal REST error: HTTP {e.code} — {body[:500]}", file=sys.stderr)
        return None
    except urllib.error.URLError as e:
        print(f"❌ Cannot reach Signal REST API at {SIGNAL_REST_URL}: {e.reason}", file=sys.stderr)
        return None


# ---------------------------------------------------------------------------
# Command functions
# ---------------------------------------------------------------------------

def cmd_signal(args):
    """Send via Signal using the remote REST API."""
    # --to is only required for direct messages (not group sends)
    if not args.to and not args.recipients:
        print("❌ Error: --to <phone> is required for direct messages, or use --recipients for groups.", file=sys.stderr)
        sys.exit(1)
    if not args.text and not getattr(args, "attach", None) and not args.recipients:
        print("❌ Error: --text <message> or --attach <file> is required.", file=sys.stderr)
        sys.exit(1)

    account = args.account or SIGNAL_DEFAULT_ACCOUNT
    if not account.startswith("+"):
        account = "+" + account

    # Build the JSON payload for /v2/send
    payload = {"number": account}

    if args.recipients:
        # Send to a Signal group — use recipients array with group. prefix
        payload["recipients"] = [args.recipients]
    else:
        # Send to individual recipients (string)
        payload["recipient"] = args.to[0]

    # Message text (join multiple --text args)
    if args.text:
        payload["message"] = "\n".join(args.text)

    # Attachments
    attach_files = getattr(args, "attach", None)

    if attach_files and len(attach_files) > 0:
        if len(attach_files) == 1:
            # Single file — send as multipart with message
            result = signal_rest_post("/v2/send", payload=payload, files=attach_files)
        else:
            # Multiple files: send message first, then each attachment separately
            multi_payload = {"number": account, "message": payload.get("message", "")}
            if args.recipients:
                multi_payload["recipients"] = [args.recipients]
            else:
                multi_payload["recipient"] = args.to[0]
            result = signal_rest_post("/v2/send", payload=multi_payload)
            if result is None:
                sys.exit(1)
            # Then send each attachment
            for fpath in attach_files:
                att_payload = {"number": account}
                if args.recipients:
                    att_payload["recipients"] = [args.recipients]
                else:
                    att_payload["recipient"] = args.to[0]
                result = signal_rest_post("/v2/send", payload=att_payload, files=[fpath])
                if result is None:
                    print(f"⚠️  Attachment failed: {fpath}", file=sys.stderr)
    elif args.text and not args.recipients:
        result = signal_rest_post("/v2/send", payload=payload)
    else:
        # Group message or other case
        result = signal_rest_post("/v2/send", payload=payload)

    if result is not None:
        if args.recipients:
            print(f"✅ Signal message sent to group {args.recipients}")
        elif args.to:
            print(f"✅ Signal message sent to {args.to[0]}")
        else:
            print("✅ Signal message sent")
        if args.verbose and result:
            # Print any extra details from the API response
            for key, value in result.items():
                if key not in ("success",):
                    print(f"   {key}: {value}")
    else:
        print("❌ Signal message send failed.", file=sys.stderr)
        sys.exit(1)


def cmd_sms(args):
    """Send via SMS/iMessage using imsg."""
    if not args.to:
        print("❌ Error: --to <phone> is required for SMS.", file=sys.stderr)
        sys.exit(1)
    if not args.text:
        print("❌ Error: --text <message> is required for SMS.", file=sys.stderr)
        sys.exit(1)

    cmd = ["imsg", "send", "--to", args.to[0]]

    if args.service:
        cmd.extend(["--service", args.service])

    msg_text = "\n".join(args.text)
    cmd.extend(["--text", msg_text])

    if getattr(args, "file", None):
        for f in args.file:
            if not os.path.isfile(f):
                print(f"⚠️  Warning: file not found: {f}", file=sys.stderr)
            cmd.extend(["--file", f])

    result = subprocess.run(cmd, capture_output=True, text=True)

    if result.returncode == 0:
        service_label = args.service.upper() if args.service else "AUTO"
        print(f"✅ SMS/iMessage sent to {args.to[0]} (service: {service_label})")
    else:
        print(f"❌ SMS send failed (exit {result.returncode})", file=sys.stderr)
        if result.stderr:
            print(f"   {result.stderr.strip()}", file=sys.stderr)


def cmd_csv(args):
    """Bulk send messages from a CSV file.

    Expected CSV columns:
      method, recipient, name, message, account, service, file, delay
    """
    filepath = args.csv

    if not os.path.isfile(filepath):
        print(f"❌ Error: file not found: {filepath}", file=sys.stderr)
        sys.exit(1)

    delay = float(args.delay) if args.delay else 0.0

    with open(filepath, newline="", encoding="utf-8") as csvfile:
        reader = csv.DictReader(csvfile)
        rows = list(reader)

    if not rows:
        print("❌ Error: CSV file is empty (no data rows).", file=sys.stderr)
        sys.exit(1)

    total = len(rows)
    success = 0
    failed = 0
    errors = []

    for i, row in enumerate(rows, 1):
        method = row.get("method", "").strip().lower()
        message = row.get("message", "").strip()
        to = row.get("recipient", "").strip()
        name = row.get("name", "").strip()

        # Skip blank rows
        if not method and not message:
            continue

        if not method:
            method = "signal"  # default

        if method not in ("signal", "sms"):
            print(f"⚠️  Row {i}: unknown method '{method}' — skipping", file=sys.stderr)
            failed += 1
            errors.append(f"Row {i}: unknown method '{method}'")
            continue

        if not message:
            print(f"⚠️  Row {i}: empty message — skipping", file=sys.stderr)
            failed += 1
            errors.append(f"Row {i}: empty message")
            continue

        # Common fields
        account = row.get("account", "").strip() or SIGNAL_DEFAULT_ACCOUNT
        service = row.get("service", "").strip() or None
        file_path = row.get("file", "").strip() or None
        delay_str = row.get("delay", "0").strip()
        row_delay = float(delay_str) if delay_str else 0.0

        label = f"{name} ({to})" if name else to or "(group)"
        print(f"[{i}/{total}] {method.upper()} → {label}")

        if method == "signal":
            group_id = to if to.startswith("group.") else None
            result = send_one_signal(
                account=account,
                recipient=to,
                message=message,
                group_id=group_id,
                attach=file_path,
            )
        elif method == "sms":
            result = send_one_sms(
                to=to,
                message=message,
                service=service,
                file=file_path,
            )
            if result:
                success += 1
            else:
                failed += 1
                errors.append(f"Row {i}: SMS failed")
            if row_delay > 0:
                time.sleep(row_delay)
            continue

        if result:
            success += 1
        else:
            failed += 1
            errors.append(f"Row {i}: failed to send")

        # Delay between sends (only if explicitly set in row)
        if row_delay > 0:
            time.sleep(row_delay)

    # Summary
    print()
    print("=" * 50)
    print("📊 CSV Send Summary")
    print("=" * 50)
    print(f"  Total rows:   {total}")
    print(f"  ✅ Success:    {success}")
    print(f"  ❌ Failed:     {failed}")
    if errors:
        print()
        print("  Errors:")
        for e in errors:
            print(f"    • {e}")
    print("=" * 50)

    if failed > 0:
        sys.exit(1)


def cmd_list_signal(args):
    """List linked Signal accounts via the REST API."""
    result = signal_rest_get("/v1/accounts")
    if result is None:
        print(f"❌ Failed to list accounts (cannot reach {SIGNAL_REST_URL})", file=sys.stderr)
        sys.exit(1)

    if isinstance(result, list):
        accounts = [str(a) for a in result]
    elif isinstance(result, dict) and "accounts" in result:
        accounts = result["accounts"]
    elif isinstance(result, dict) and "number" in result:
        accounts = [result["number"]]
    else:
        accounts = [str(result)]

    if not accounts:
        print("No Signal accounts linked. Use --link-signal to set one up.")
    else:
        print("Linked Signal accounts:")
        for acct in accounts:
            print(f"  • {acct}")


def cmd_link_signal(args):
    """Link a new Signal device using the sgnl:// URL method.

    The signal-cli-rest-api supports device linking via its /v1/link endpoint.
    This prints the sgnl:// URL to stdout, then waits for the user to scan/confirm.
    """
    name = args.name or "automation"
    payload = {"name": name}
    result = signal_rest_post("/v1/link", payload=payload)

    if result is None:
        print("❌ Failed to initiate device linking.", file=sys.stderr)
        sys.exit(1)

    sgnl_url = result.get("url", result.get("linkUrl", ""))
    if not sgnl_url:
        print("❌ No sgnl:// URL returned. API response:", file=sys.stderr)
        print(json.dumps(result, indent=2), file=sys.stderr)
        sys.exit(1)

    print(f"🔗 Scan this QR code or visit this URL to link your device:")
    print(f"   {sgnl_url}")
    print()
    print("Waiting for confirmation (Ctrl+C to cancel)...")

    start = time.time()
    while time.time() - start < 120:
        time.sleep(3)
        result = signal_rest_get("/v1/accounts")
        if result and (isinstance(result, list) and len(result) > 0):
            print(f"✅ Device linked! Account: {result}")
            return

    print("⚠️  Linking timed out. Please try again.", file=sys.stderr)


# ---------------------------------------------------------------------------
# Main / argument parsing
# ---------------------------------------------------------------------------

def main():
    parser = argparse.ArgumentParser(
        prog="sendmsg",
        description="Universal message sender for Signal (REST API) and SMS/iMessage",
        epilog=(
            "Examples:\n"
            "  %(prog)s --signal --to +18005551212 --text 'Hello!'\n"
            "  %(prog)s --signal --recipients GROUP_ID --text 'Hello group!'\n"
            "  %(prog)s --sms   --to +18005551212 --text 'Text via SMS'\n"
            "  %(prog)s --sms   --to +18005551212 --text 'Hey' --file ~/pic.jpg\n"
            "  %(prog)s --csv messages.csv\n"
            "  %(prog)s --csv messages.csv --delay 2\n"
            "  %(prog)s --list-signal\n"
            "  %(prog)s --link-signal\n"
            "  %(prog)s --link-signal --name 'my-automation'\n"
            "\n"
            "CSV columns: method, recipient, name, message, account, service, file, delay\n"
            "The 'name' field is printed in the status output during CSV sends.\n"
            f"Signal REST API: {SIGNAL_REST_URL}\n"
            "Config file: ~/.sendmsg.conf\n"
            "  [settings]\n"
            "  signal_rest_url = http://localhost:8080\n"
            "  signal_default_account = +1888222333\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )

    parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {VERSION}")

    # Method (mutually exclusive)
    method = parser.add_argument_group("Messaging method (choose one)")
    method.add_argument("--signal", action="store_true", help="Send via Signal")
    method.add_argument("--sms", action="store_true", help="Send via SMS/iMessage (macOS Messages)")
    method.add_argument("--csv", metavar="FILE", help="Bulk send from a CSV file")

    # Common options
    parser.add_argument("--to", nargs="+", metavar="PHONE", help="Recipient phone number(s)")
    parser.add_argument("--text", nargs="+", metavar="MSG", help="Message text (repeat for multiple)")
    parser.add_argument("--file", "--attach", nargs="+", metavar="PATH", help="Attachment file path(s)")
    parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")

    # Signal-specific
    signal_grp = parser.add_argument_group("Signal options")
    signal_grp.add_argument("--account", metavar="PHONE", help="Signal account phone number to use")
    signal_grp.add_argument("--recipients", metavar="RECIPIENTS", help="Send to Signal group ID (no --to needed)")
    signal_grp.add_argument("--voice", action="store_true", help="Send as voice note (Signal only)")

    # Management commands
    mgmt = parser.add_argument_group("Management commands")
    mgmt.add_argument("--list-signal", action="store_true", help="List linked Signal accounts")
    mgmt.add_argument("--link-signal", action="store_true", help="Link a new device to your Signal account")
    signal_grp.add_argument("--name", metavar="NAME", help="Device name for Signal linking (default: 'automation')")

    # SMS-specific
    sms_grp = parser.add_argument_group("SMS/iMessage options")
    sms_grp.add_argument("--service", choices=["imessage", "sms", "auto"], help="Force service (default: auto-detect)")

    # CSV-specific
    csv_grp = parser.add_argument_group("CSV options")
    csv_grp.add_argument("--delay", metavar="SECONDS", help="Wait N seconds between CSV row sends (default: 0)")

    args = parser.parse_args()

    # Validate: must have a method or management command
    if not any([args.signal, args.sms, args.csv, args.list_signal, args.link_signal]):
        parser.print_help()
        print("\n❌ Error: Specify --signal, --sms, --csv, --list-signal, or --link-signal", file=sys.stderr)
        sys.exit(1)

    if sum([args.signal, args.sms]) > 1:
        print("❌ Error: Choose only one method (--signal or --sms).", file=sys.stderr)
        sys.exit(1)

    # Set defaults
    if args.link_signal and not args.name:
        args.name = "automation"

    # Dispatch
    if args.signal:
        cmd_signal(args)
    elif args.sms:
        cmd_sms(args)
    elif args.csv:
        cmd_csv(args)
    elif args.list_signal:
        cmd_list_signal(args)
    elif args.link_signal:
        cmd_link_signal(args)


if __name__ == "__main__":
    main()