#!/usr/bin/env python3

import logging, os
log_level = os.environ.get('LOGLEVEL', 'WARNING').upper()

logging.basicConfig(
    level=getattr(logging, log_level, 'WARNING'),
    format="%(asctime)s - %(name)s - %(levelname)s: %(filename)s:%(lineno)d - %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger('bluesky_posting')

import argparse
import base64
from datetime import datetime, timezone
import json
from pathlib import Path
import re
import requests
import sys
from typing import Dict, List
from urllib.parse import unquote
import yaml

import bluesky_post
"""
pip install 'requests' and 'bs4' (BeautifulSoup)

only CLI input required is
Bluesky and GitHub credentials if not defined in the Environment

"""
# load config file
def load_config(path):
    with open(path) as infile:
        return yaml.safe_load(infile)

# create web-safe filepaths
def scrub_path(filepath):
    return re.sub(r'([ _?\#%"]+)', '_', filepath)

def format_embed_url(filepath):
    logger.debug(f"filepath: {filepath}")
    relative_url = scrub_path(Path(filepath).with_suffix('.html').as_posix())
    logger.debug(f"the relative url: {relative_url}")
    return relative_url

def get_relative_filename(filename_input, repo_name):
    """
    Convert an absolute filename, Obsidian URL, or filename to a repository-relative filename
    Args:
        filename_input (str): The input filename: an absolute path, an Obsidian vault URL, or a filename fragment
        repo_name (str): The name of the repository to base the relative path on
    Returns:
        str or None: The relative filename path if successful, None if an error occurs
    Raises:
        FileNotFoundError: If the specified file cannot be found
    """
    logger.debug(f"filename_input: {filename_input}")
    logger.debug(f"repo_name: {repo_name}")    
    if filename_input.startswith(Path.home().as_posix()):
        try:
            if not Path(filename_input).exists():
                raise FileNotFoundError(f"The file \'{filename_input}\' does not exist")
            parts = Path(filename_input).parts
            repo_path = Path(*parts[parts.index('/'):parts.index(repo_name)+1])
            logger.debug(f"repo_path: {repo_path}")
            relative_filename = Path(filename_input).relative_to(repo_path).as_posix()
            logger.debug(f"relative_filename: {relative_filename}")
            return relative_filename
        except FileNotFoundError as e:
            print(f"Error: {e}", file=sys.stderr)
            return None
        except Exception as e:
            print(f"An unexpected error occurred: {e}", file=sys.stderr)
            return None

    try:
        if filename_input.startswith('obsidian://'):
            vault_match = re.search(r'vault=([^&]+)', filename_input)
            vault = vault_match.group(1) if vault_match else ""
            file_match = re.search(r'file=([^&]+)', filename_input)
            filename = unquote(file_match.group(1)) if file_match else ""
            logger.debug(f"Vault: {vault}")
            logger.debug(f"File: {filename}")
            file_path = list(Path.home().rglob(f"*/{vault}/{filename}.md"))
            if not file_path:
                raise FileNotFoundError(f"No file named '{vault}/{filename}.md' found.")
            relative_filename = f"{filename}.md"
            logger.debug(f"relative_filename: {relative_filename}")
            return relative_filename

        repo_path = list(Path.home().rglob(f"*/{repo_name}"))[0]
        logger.debug(f"repo_path: {repo_path}")
        file_path = list(repo_path.rglob(filename_input))
        logger.debug(f"file_path: {file_path}")
        if not file_path:
            raise FileNotFoundError(f"No file named \'{filename_input}\' found.")
        relative_filename = file_path[0].relative_to(repo_path).as_posix()
        return relative_filename

    except FileNotFoundError as e:
        print(f"Error: {e}", file=sys.stderr)
        return None
    except Exception as e:
        print(f"An unexpected error occurred: {e}", file=sys.stderr)
        return None

def trim_with_ellipsis(text, limit):
    """
    Trim text to a previous word boundary when it exceeds the character limit
    and add "..." at the end.
    """
    # Find the last space before the limit
    if (last_space := text[:limit].rfind(' ')) == -1:
        return text[:limit] + "..."
    # Trim to the last complete word and add ellipsis
    return text[:last_space] + "..."

def get_valid_post(char_limit):
    """
    Prompt for text for a Bluesky post, ensuring it stays within
    the character limit or offering to truncate it.
    Args:
        char_limit (int): The maximum number of characters allowed for the post
    Returns:
        str: Valid post text within the character limit
    Raises:
        SystemExit: If the user interrupts with CTRL-C
    """
    while True:
        try:
            text = input(f"Enter bluesky post text ({char_limit} characters available): ")
            if len(text) <= char_limit:
                return text

            print(f"The text is {len(text)} characters long, and exceeds the {char_limit} character limit.")
            choice = input("The options are (1) re-enter a shorter text or (2) truncate the text. Enter 1 or 2: ")
            match choice:
                case "1":
                    continue
                case "2":
                    truncated_text = trim_with_ellipsis(text, char_limit)
                    print(f"The text is truncated to: {truncated_text}")
                    return truncated_text
                case _:
                    print("Invalid option; please enter 1 or 2.")
        except KeyboardInterrupt:
            print("\nCTRL-C detected; exiting.")
            exit()

def get_filename():
    try:
        filename = input("Enter the Markdown file name: ").strip()
        if (filename.startswith('"') and filename.endswith('"')) or (filename.startswith("'") and filename.endswith("'")):
            filename = filename[1:-1]
        return filename
    except KeyboardInterrupt:
        print("\nCTRL-C detected; exiting.")
        exit()

def update_github_file_api(repo_name, file_path, new_content, commit_message, token=None):
    """
    Update a file in a GitHub repository by adding to its YAML frontmatter.
    If frontmatter exists, it adds to it; otherwise, it creates new frontmatter.
    Args:
        repo_name (str): Repository name in format 'username/repo'
        file_path (str): Path to the file in the repository
        new_content (str): Bluesky post URL to add to the file
        commit_message (str): Message to use for the commit
        token (str, optional): GitHub personal access token. If None, uses GH_TOKEN environment variable
    Returns:
        bool: True if successful, False otherwise
    Raises:
        ValueError: If GitHub token is not provided and not in environment variables
        requests.exceptions.RequestException: If the API request fails
    """
    try:
        # Get GitHub token
        if token is None:
            token = os.environ.get('GH_TOKEN')
            if not token:
                raise ValueError("GitHub token not provided and GH_TOKEN environment variable not set")
        
        # API URLs
        api_url = f"https://api.github.com/repos/{repo_name}/contents/{file_path}"
        headers = {
            "Authorization": f"token {token}",
            "Accept": "application/vnd.github.v3+json"
        }
        
        # Get the file to retrieve its SHA and content if appending
        response = requests.get(api_url, headers=headers)
        response.raise_for_status()
        
        # Extract the SHA and current content if appending
        file_data = response.json()
        file_sha = file_data["sha"]
        
        # Decode the current content (it is in base64)
        current_content = base64.b64decode(file_data["content"]).decode("utf-8")
        # Insert bluesky_comments_post URL as YAML frontmatter
        # check for existing YAML frontmatter
        if current_content.startswith("---\n"):
            content_to_upload = f"---\nbluesky_comments_post: {new_content}\n" + current_content[4:]
        else:
             bsky_post_content = f"---\nbluesky_comments_post: {new_content}\n---\n"
             content_to_upload = bsky_post_content + current_content
        
        content_bytes = content_to_upload.encode("utf-8")
        base64_content = base64.b64encode(content_bytes).decode("utf-8")
        
        update_data = {
            "message": commit_message,
            "content": base64_content,
            "sha": file_sha
        }
        
        response = requests.put(api_url, headers=headers, data=json.dumps(update_data))
        response.raise_for_status()
        logger.info(f"✅ Successfully added to {file_path} in {repo_name}")
        return True
        
    except requests.exceptions.RequestException as e:
        error_msg = str(e)
        if hasattr(e, 'response') and e.response:
            try:
                error_detail = e.response.json()
                error_msg = f"{str(e)} - {json.dumps(error_detail)}"
            except:
                pass
        print(f"❌ API request failed: {error_msg}")
        return False
    except Exception as e:
        print(f"❌ Error: {str(e)}")
        return False

def main():
    # read 'bskypost.yaml' config file
    # 
    # if site or repo not defined prompt for CLI input
    parser = argparse.ArgumentParser(description="Post MarkPub webpage to Bluesky and update Markdown page with bluesky-post URL")
    # Bluesky post arguments
    parser.add_argument(
        "--pds-url", metavar="BLUESKY_HOST", default=os.environ.get("ATP_PDS_HOST") or "https://bsky.social"
    )
    parser.add_argument("--handle", metavar="BLUESKY_HANDLE", default=os.environ.get("ATP_AUTH_HANDLE"))
    parser.add_argument("--password", metavar="BLUESKY_PASSWORD", default=os.environ.get("ATP_AUTH_PASSWORD"))
    # GitHub API arguments
    parser.add_argument("--token", metavar="GITHUB_TOKEN", default=os.environ.get("GH_TOKEN"))
#    parser.add_argument("--sitehost", default=config.get('deploy_site', ''))
#    parser.add_argument("--reponame", default=config.get('repo_name', ''))
    args = parser.parse_args()
    logger.debug(f"args: {args}")

    if not (args.handle and args.password):
        logger.critical("both Bluesky handle and password are required")
        return -1
    
    if not (args.token):
        logger.critical("GitHub access token is required")
        return -1

    #get config info: deploy_site, repo_name
    config = load_config(Path('./bskypost.yaml').resolve().as_posix())
    logger.debug(f"config info: {config}")

    # get filename and embed_url path
    markdown_filename = get_filename()
    logger.debug(f"markdown_filename: {markdown_filename}")
    relative_filename = get_relative_filename(markdown_filename, config['repo_name'].split("/")[-1])
    logger.debug(f"relative_filename: {relative_filename}")
    if relative_filename:
        embed_url_path = format_embed_url(relative_filename)
        if 'github.io' in config['deploy_site']:
            embed_url = f"https://{config['deploy_site']}/{config['repo_name'].split("/")[-1]}/{embed_url_path}"
        else:
            embed_url = f"https://{config['deploy_site']}/{embed_url_path}"
        # use the character count of the embed_url to display limit on text input
        post_text = get_valid_post(299-len(embed_url))
    else:
        print("MarkPub page URL processing failed. Exiting.")
        return 1

    setattr(args, 'embed_url', embed_url)
    setattr(args, 'text', post_text)
    logger.debug(f"updated args: {args}")

    bsky_post_url = bluesky_post.create_post(args)
    logger.debug(f"bluesky_post return: {bsky_post_url}")
    
    success = update_github_file_api(
        repo_name=config['repo_name'],
        file_path=relative_filename,
        new_content=bsky_post_url,
        commit_message="add bluesky post URL frontmatter",
        token=args.token
    )
    
    if not success:
        return 1

if __name__ == "__main__":
    exit(main())

