#!/usr/bin/env python3
"""
Axiom Zero — Dynamic DNS Updater
Runs on Pi 5. Polls current public IP every 5 minutes.
When IP changes, updates noctualabs.tech DNS A record via your DNS provider API.

Supports: Namecheap, Cloudflare (DNS only, not proxy), Porkbun, Squarespace DNS,
          or any generic DDNS URL (e.g. DuckDNS).

Configure via /home/cade/.axiom-zero/ddns_config.json
"""

import os
import json
import time
import logging
import urllib.request
import urllib.parse
import subprocess

logging.basicConfig(
    level=logging.INFO,
    format="[DDNS] %(asctime)s %(levelname)s %(message)s",
    handlers=[
        logging.FileHandler("/home/cade/.axiom-zero/ddns.log"),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger("ddns")

CONFIG_PATH = os.path.expanduser("~/.axiom-zero/ddns_config.json")
STATE_PATH  = os.path.expanduser("~/.axiom-zero/ddns_last_ip.txt")
CHECK_INTERVAL = 300  # 5 minutes


def get_public_ip() -> str:
    """Get current public IP from multiple fallback sources."""
    sources = [
        "https://api.ipify.org",
        "https://icanhazip.com",
        "https://checkip.amazonaws.com",
        "https://ipecho.net/plain",
    ]
    for url in sources:
        try:
            with urllib.request.urlopen(url, timeout=5) as r:
                ip = r.read().decode().strip()
                if ip and "." in ip:
                    return ip
        except Exception:
            continue
    raise RuntimeError("Could not determine public IP from any source")


def load_last_ip() -> str:
    try:
        with open(STATE_PATH) as f:
            return f.read().strip()
    except FileNotFoundError:
        return ""


def save_last_ip(ip: str):
    os.makedirs(os.path.dirname(STATE_PATH), exist_ok=True)
    with open(STATE_PATH, "w") as f:
        f.write(ip)


def load_config() -> dict:
    if not os.path.exists(CONFIG_PATH):
        logger.warning(f"No DDNS config found at {CONFIG_PATH}. Creating template.")
        template = {
            "provider": "duckdns",
            "comment": "Set provider to: duckdns | namecheap | generic_url",

            "duckdns": {
                "token": "YOUR_DUCKDNS_TOKEN",
                "domain": "noctualabs",
                "comment": "Free at duckdns.org — gives you noctualabs.duckdns.org"
            },

            "namecheap": {
                "host": "api",
                "domain": "noctualabs.tech",
                "password": "YOUR_NAMECHEAP_DDNS_PASSWORD",
                "comment": "Enable DDNS in Namecheap dashboard first"
            },

            "generic_url": {
                "url": "https://example.com/ddns?token=TOKEN&ip={ip}",
                "comment": "Replace with your provider's DDNS update URL"
            }
        }
        os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
        with open(CONFIG_PATH, "w") as f:
            json.dump(template, f, indent=2)
        return template
    with open(CONFIG_PATH) as f:
        return json.load(f)


def update_dns(ip: str, config: dict) -> bool:
    """Push IP update to DNS provider. Returns True on success."""
    provider = config.get("provider", "duckdns")

    if provider == "duckdns":
        cfg = config["duckdns"]
        url = (f"https://www.duckdns.org/update"
               f"?domains={cfg['domain']}&token={cfg['token']}&ip={ip}")
        try:
            with urllib.request.urlopen(url, timeout=10) as r:
                resp = r.read().decode().strip()
            logger.info(f"DuckDNS update → {resp}")
            return resp == "OK"
        except Exception as e:
            logger.error(f"DuckDNS update failed: {e}")
            return False

    elif provider == "namecheap":
        cfg = config["namecheap"]
        url = (f"https://dynamicdns.park-your-domain.com/update"
               f"?host={cfg['host']}&domain={cfg['domain']}"
               f"&password={cfg['password']}&ip={ip}")
        try:
            with urllib.request.urlopen(url, timeout=10) as r:
                resp = r.read().decode()
            ok = "<ErrCount>0</ErrCount>" in resp
            logger.info(f"Namecheap DDNS → {'OK' if ok else 'ERROR: ' + resp[:200]}")
            return ok
        except Exception as e:
            logger.error(f"Namecheap update failed: {e}")
            return False

    elif provider == "generic_url":
        cfg = config["generic_url"]
        url = cfg["url"].format(ip=ip)
        try:
            with urllib.request.urlopen(url, timeout=10) as r:
                resp = r.read().decode().strip()
            logger.info(f"Generic DDNS update → {resp[:100]}")
            return True
        except Exception as e:
            logger.error(f"Generic DDNS update failed: {e}")
            return False

    else:
        logger.error(f"Unknown provider: {provider}")
        return False


def main():
    logger.info("=== Axiom Zero Dynamic DNS Updater started ===")
    config = load_config()
    logger.info(f"Provider: {config.get('provider')}")

    while True:
        try:
            current_ip = get_public_ip()
            last_ip = load_last_ip()

            if current_ip != last_ip:
                logger.info(f"IP changed: {last_ip or 'none'} → {current_ip}")
                if update_dns(current_ip, config):
                    save_last_ip(current_ip)
                    logger.info(f"DNS updated to {current_ip} ✅")
                else:
                    logger.warning("DNS update failed — will retry in 60s")
                    time.sleep(60)
                    continue
            else:
                logger.debug(f"IP unchanged: {current_ip}")

        except Exception as e:
            logger.error(f"Error in DDNS loop: {e}")

        time.sleep(CHECK_INTERVAL)


if __name__ == "__main__":
    main()
