#!/usr/bin/env python3
"""
Notion Writer - Push markdown content to Notion pages or databases.
Supports both page content updates and database row creation.
"""

import os
import sys
import re
import json
import argparse
from pathlib import Path
from typing import Optional, List, Dict, Any, Union

from dotenv import load_dotenv
from notion_client import Client
from notion_client.errors import APIResponseError

import _notion_compat as compat

# Load environment variables
load_dotenv(Path(__file__).parent / '.env')

NOTION_TOKEN = os.getenv('NOTION_API_KEY') or os.getenv('NOTION_TOKEN')


def extract_notion_id(url_or_id: str) -> Optional[str]:
    """Extract Notion page/database ID from URL or raw ID."""
    # Already a raw ID (32 chars hex, with or without dashes)
    clean_id = url_or_id.replace('-', '')
    if re.match(r'^[a-f0-9]{32}$', clean_id):
        return clean_id

    # Notion URL patterns
    patterns = [
        r'notion\.so/(?:[^/]+/)?([a-f0-9]{32})',  # notion.so/workspace/page-id
        r'notion\.so/(?:[^/]+/)?[^-]+-([a-f0-9]{32})',  # notion.so/workspace/Page-Title-id
        r'notion\.site/(?:[^/]+/)?([a-f0-9]{32})',  # public notion.site
        r'notion\.site/(?:[^/]+/)?[^-]+-([a-f0-9]{32})',
        r'([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})',  # UUID format
    ]

    for pattern in patterns:
        match = re.search(pattern, url_or_id)
        if match:
            return match.group(1).replace('-', '')

    return None


def format_id_with_dashes(raw_id: str) -> str:
    """Format 32-char ID to UUID format with dashes."""
    if len(raw_id) == 32:
        return f"{raw_id[:8]}-{raw_id[8:12]}-{raw_id[12:16]}-{raw_id[16:20]}-{raw_id[20:]}"
    return raw_id


TABLE_SEPARATOR_RE = re.compile(r'^\s*\|?(\s*:?-{3,}:?\s*\|)+\s*:?-{3,}:?\s*\|?\s*$')


def _is_table_row(line: str) -> bool:
    stripped = line.strip()
    return stripped.startswith('|') and stripped.endswith('|') and stripped.count('|') >= 2


def _split_table_row(line: str) -> List[str]:
    stripped = line.strip()
    if stripped.startswith('|'):
        stripped = stripped[1:]
    if stripped.endswith('|'):
        stripped = stripped[:-1]
    return [cell.strip() for cell in stripped.split('|')]


def markdown_to_notion_blocks(content: Union[str, List[str]]) -> List[Dict[str, Any]]:
    """Convert markdown text (or pre-split lines) to Notion block objects.

    Reentrant: container detectors recursively call _parse_lines on inner content.
    """
    if isinstance(content, str):
        lines = content.split('\n')
    else:
        lines = list(content)
    return _parse_lines(lines)


def _parse_lines(lines: List[str]) -> List[Dict[str, Any]]:
    """Walk lines and emit Notion blocks. Called recursively by container detectors."""
    blocks = []
    i = 0
    while i < len(lines):
        line = lines[i]
        if not line.strip():
            i += 1
            continue

        # Toggle (HTML5 <details>) — multi-line form, depth-tracked for nesting
        if line.strip() == '<details>':
            i += 1
            # Optional <summary>...</summary> on next non-blank line
            summary_text = ''
            while i < len(lines) and not lines[i].strip():
                i += 1
            if i < len(lines):
                summary_match = re.match(r'^\s*<summary>(.*)</summary>\s*$', lines[i])
                if summary_match:
                    summary_text = summary_match.group(1)
                    i += 1
            # Collect body lines until matching </details>, depth-tracked
            inner_lines: List[str] = []
            depth = 1
            while i < len(lines) and depth > 0:
                stripped_inner = lines[i].strip()
                if stripped_inner == '<details>':
                    depth += 1
                    inner_lines.append(lines[i])
                elif stripped_inner == '</details>':
                    depth -= 1
                    if depth > 0:
                        inner_lines.append(lines[i])
                else:
                    inner_lines.append(lines[i])
                i += 1
            if depth > 0:
                # Unclosed <details> at EOF — degrade to paragraphs
                print("Warning: unclosed <details> at EOF; emitting body as paragraphs",
                      file=sys.stderr)
                if summary_text:
                    blocks.append(create_paragraph_block(summary_text))
                for inner in inner_lines:
                    if inner.strip():
                        blocks.append(create_paragraph_block(inner))
                continue
            children = _parse_lines(inner_lines)
            blocks.append(create_toggle_block(summary_text, children))
            continue

        # Columns (Pandoc fenced div ::: columns)
        if line.strip() == '::: columns':
            columns_lines: List[List[str]] = []
            current_col: Optional[List[str]] = None
            i += 1
            depth = 1  # we are inside our own wrapper
            while i < len(lines) and depth > 0:
                stripped_col = lines[i].strip()
                if stripped_col == '::: columns':
                    depth += 1
                    if current_col is not None:
                        current_col.append(lines[i])
                    i += 1
                    continue
                if stripped_col == '::: column':
                    depth += 1
                    if depth == 2:
                        # Top-level column inside our wrapper
                        if current_col is not None:
                            columns_lines.append(current_col)
                        current_col = []
                    else:
                        # Column inside a nested wrapper — record verbatim
                        if current_col is not None:
                            current_col.append(lines[i])
                    i += 1
                    continue
                if stripped_col == ':::':
                    depth -= 1
                    if depth == 0:
                        # Closing our wrapper
                        if current_col is not None:
                            columns_lines.append(current_col)
                            current_col = None
                        i += 1
                        break
                    if depth == 1:
                        # Closing a top-level column
                        if current_col is not None:
                            columns_lines.append(current_col)
                            current_col = None
                    else:
                        # Closing something nested — record verbatim
                        if current_col is not None:
                            current_col.append(lines[i])
                    i += 1
                    continue
                # Regular content line — append to current column if open
                if current_col is not None:
                    current_col.append(lines[i])
                i += 1
            if depth > 0:
                # Unclosed wrapper at EOF — degrade to paragraphs
                print("Warning: unclosed ::: columns at EOF; emitting as paragraphs",
                      file=sys.stderr)
                if current_col is not None:
                    columns_lines.append(current_col)
                for col in columns_lines:
                    for inner in col:
                        if inner.strip():
                            blocks.append(create_paragraph_block(inner))
                continue
            # Drop columns that are pure whitespace
            columns_lines = [c for c in columns_lines if any(li.strip() for li in c)]
            if len(columns_lines) < 2:
                # Notion requires >= 2 columns; single column degrades to paragraphs
                for col in columns_lines:
                    for inner in col:
                        if inner.strip():
                            blocks.append(create_paragraph_block(inner))
                continue
            column_blocks = [_parse_lines(col_lines) for col_lines in columns_lines]
            blocks.append(create_column_list_block(column_blocks))
            continue

        if _is_table_row(line) and i + 1 < len(lines) and TABLE_SEPARATOR_RE.match(lines[i + 1]):
            header_cells = _split_table_row(line)
            i += 2
            body_rows: List[List[str]] = []
            while i < len(lines) and _is_table_row(lines[i]):
                body_rows.append(_split_table_row(lines[i]))
                i += 1
            blocks.append(create_table_block(header_cells, body_rows))
            continue
        if line.startswith('######'):
            blocks.append(create_heading_block(line[6:].strip(), 3))
        elif line.startswith('#####'):
            blocks.append(create_heading_block(line[5:].strip(), 3))
        elif line.startswith('####'):
            blocks.append(create_heading_block(line[4:].strip(), 3))
        elif line.startswith('###'):
            blocks.append(create_heading_block(line[3:].strip(), 3))
        elif line.startswith('##'):
            blocks.append(create_heading_block(line[2:].strip(), 2))
        elif line.startswith('#'):
            blocks.append(create_heading_block(line[1:].strip(), 1))
        elif line.startswith('```'):
            language = line[3:].strip() or 'plain text'
            code_lines = []
            i += 1
            while i < len(lines) and not lines[i].startswith('```'):
                code_lines.append(lines[i])
                i += 1
            blocks.append(create_code_block('\n'.join(code_lines), language))
        # Checkbox / Todo (must come before generic bullet match)
        elif line.strip().startswith('- [ ]'):
            text = line.strip()[5:].strip()
            blocks.append(create_todo_block(text, False))
        elif line.strip().startswith('- [x]') or line.strip().startswith('- [X]'):
            text = line.strip()[5:].strip()
            blocks.append(create_todo_block(text, True))
        elif line.strip().startswith('- ') or line.strip().startswith('* '):
            text = line.strip()[2:]
            blocks.append(create_bulleted_list_block(text))
        elif re.match(r'^\d+\.\s', line.strip()):
            text = re.sub(r'^\d+\.\s', '', line.strip())
            blocks.append(create_numbered_list_block(text))
        # Blockquote — GitHub-alert callout takes priority over generic quote
        elif line.startswith('>'):
            first_body = line[1:].strip()
            alert_match = ALERT_RE.match(first_body)
            if alert_match:
                alert_type = alert_match.group(1)
                body_lines: List[str] = []
                i += 1
                while i < len(lines) and lines[i].lstrip().startswith('>'):
                    body_lines.append(lines[i].lstrip()[1:].lstrip())
                    i += 1
                body_text = '\n'.join(body_lines)
                blocks.append(create_callout_block(body_text, alert_type))
                continue
            blocks.append(create_quote_block(first_body))
        elif line.strip() in ['---', '***', '___']:
            blocks.append(create_divider_block())
        else:
            blocks.append(create_paragraph_block(line))
        i += 1
    return blocks


INLINE_PATTERNS = [
    ('code',          re.compile(r'`([^`\n]+)`')),
    ('link',          re.compile(r'\[([^\]]+)\]\(([^)\s]+)\)')),
    ('bold_star',     re.compile(r'\*\*([^*\n]+)\*\*')),
    ('bold_under',    re.compile(r'__([^_\n]+)__')),
    ('strike',        re.compile(r'~~([^~\n]+)~~')),
    ('italic_star',   re.compile(r'\*([^*\n]+)\*')),
    ('italic_under',  re.compile(r'(?<![a-zA-Z0-9_])_([^_\n]+)_(?![a-zA-Z0-9_])')),
]

ALERT_TYPES = {
    'NOTE':      ('ℹ️', 'blue_background'),
    'TIP':       ('💡', 'green_background'),
    'IMPORTANT': ('☝️', 'purple_background'),
    'WARNING':   ('⚠️', 'yellow_background'),
    'CAUTION':   ('🚨', 'red_background'),
}
ALERT_RE = re.compile(r'^\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*$')


_ABSOLUTE_URL_RE = re.compile(r'^(?:[a-zA-Z][a-zA-Z0-9+.-]*:)')


def _is_absolute_url(url: str) -> bool:
    """Notion's link annotation rejects fragment-only and relative URLs."""
    return bool(_ABSOLUTE_URL_RE.match(url))


def _rich_span(content: str, **annotations: Any) -> Dict[str, Any]:
    """Build one rich-text span with optional annotations / link."""
    span: Dict[str, Any] = {
        "type": "text",
        "text": {"content": content[:2000]},
    }
    link = annotations.pop('link', None)
    if link:
        span["text"]["link"] = {"url": link}
    if annotations:
        span["annotations"] = {k: v for k, v in annotations.items() if v}
    return span


def parse_rich_text(text: str) -> List[Dict[str, Any]]:
    """Parse markdown inline formatting into Notion rich-text spans.

    Supports: **bold**, __bold__, *italic*, _italic_, `code`, ~~strike~~, [text](url).
    No nesting (Notion rich-text flattens annotations onto each span anyway).
    """
    if not text:
        return []

    spans: List[Dict[str, Any]] = []
    remaining = text

    while remaining:
        # Find the earliest inline token in what's left.
        best: Optional[tuple] = None  # (kind, match_obj)
        for kind, pattern in INLINE_PATTERNS:
            m = pattern.search(remaining)
            if m and (best is None or m.start() < best[1].start()):
                best = (kind, m)

        if not best:
            spans.append(_rich_span(remaining))
            break

        kind, m = best
        if m.start() > 0:
            spans.append(_rich_span(remaining[:m.start()]))

        if kind == 'code':
            spans.append(_rich_span(m.group(1), code=True))
        elif kind == 'link':
            link_text, link_url = m.group(1), m.group(2)
            if _is_absolute_url(link_url):
                spans.append(_rich_span(link_text, link=link_url))
            elif link_url.startswith('#'):
                # TOC-style fragment link — Notion rejects, preserve nav intent as bold.
                spans.append(_rich_span(link_text, bold=True))
            else:
                # Relative path or unrecognized scheme — drop link, keep text.
                spans.append(_rich_span(link_text))
        elif kind in ('bold_star', 'bold_under'):
            spans.append(_rich_span(m.group(1), bold=True))
        elif kind == 'strike':
            spans.append(_rich_span(m.group(1), strikethrough=True))
        elif kind in ('italic_star', 'italic_under'):
            spans.append(_rich_span(m.group(1), italic=True))

        remaining = remaining[m.end():]

    return spans


def create_paragraph_block(text: str) -> Dict[str, Any]:
    return {
        "object": "block",
        "type": "paragraph",
        "paragraph": {
            "rich_text": parse_rich_text(text)
        }
    }


def create_heading_block(text: str, level: int) -> Dict[str, Any]:
    heading_type = f"heading_{level}"
    return {
        "object": "block",
        "type": heading_type,
        heading_type: {
            "rich_text": parse_rich_text(text)
        }
    }


def create_bulleted_list_block(text: str) -> Dict[str, Any]:
    return {
        "object": "block",
        "type": "bulleted_list_item",
        "bulleted_list_item": {
            "rich_text": parse_rich_text(text)
        }
    }


def create_numbered_list_block(text: str) -> Dict[str, Any]:
    return {
        "object": "block",
        "type": "numbered_list_item",
        "numbered_list_item": {
            "rich_text": parse_rich_text(text)
        }
    }


def create_todo_block(text: str, checked: bool) -> Dict[str, Any]:
    return {
        "object": "block",
        "type": "to_do",
        "to_do": {
            "rich_text": parse_rich_text(text),
            "checked": checked
        }
    }


def create_quote_block(text: str) -> Dict[str, Any]:
    return {
        "object": "block",
        "type": "quote",
        "quote": {
            "rich_text": parse_rich_text(text)
        }
    }


def create_callout_block(text: str, alert_type: str) -> Dict[str, Any]:
    """Create a callout block with icon + color from a GitHub alert type."""
    icon, color = ALERT_TYPES[alert_type]
    return {
        "object": "block",
        "type": "callout",
        "callout": {
            "rich_text": parse_rich_text(text),
            "icon": {"type": "emoji", "emoji": icon},
            "color": color,
        },
    }


def create_toggle_block(summary_text: str, children_blocks: List[Dict[str, Any]]) -> Dict[str, Any]:
    """Create a toggle block with summary rich-text and child blocks."""
    return {
        "object": "block",
        "type": "toggle",
        "toggle": {
            "rich_text": parse_rich_text(summary_text),
            "children": children_blocks,
        },
    }


def create_column_list_block(columns: List[List[Dict[str, Any]]]) -> Dict[str, Any]:
    """Create a column_list block; each item in `columns` is the children-list for one column."""
    return {
        "object": "block",
        "type": "column_list",
        "column_list": {
            "children": [
                {"object": "block", "type": "column", "column": {"children": col_blocks}}
                for col_blocks in columns
            ],
        },
    }


def create_code_block(code: str, language: str) -> Dict[str, Any]:
    return {
        "object": "block",
        "type": "code",
        "code": {
            "rich_text": parse_rich_text(code),
            "language": language.lower()
        }
    }


def create_divider_block() -> Dict[str, Any]:
    return {
        "object": "block",
        "type": "divider",
        "divider": {}
    }


def create_table_block(header_cells: List[str], body_rows: List[List[str]]) -> Dict[str, Any]:
    """Build a Notion `table` block with header + body rows.

    All rows are padded to the header width so Notion accepts the payload.
    Each cell renders through `parse_rich_text` so inline formatting works
    inside tables too.
    """
    width = max(len(header_cells), max((len(r) for r in body_rows), default=0))

    def row_block(cells: List[str]) -> Dict[str, Any]:
        padded = cells + [''] * (width - len(cells))
        return {
            "object": "block",
            "type": "table_row",
            "table_row": {
                "cells": [parse_rich_text(c) for c in padded]
            },
        }

    children = [row_block(header_cells)] + [row_block(r) for r in body_rows]

    return {
        "object": "block",
        "type": "table",
        "table": {
            "table_width": width,
            "has_column_header": True,
            "has_row_header": False,
            "children": children,
        },
    }


def list_accessible_content(notion: Client, filter_type: str = 'all') -> None:
    """List all accessible pages and databases."""
    print("=" * 70)
    print("Accessible Notion Content (Claude-D.intelligence)")
    print("=" * 70)

    databases = []
    pages = []

    try:
        # Search all content and filter locally
        response = notion.search()
        all_results = response.get('results', [])

        # Handle pagination
        while response.get('has_more'):
            response = notion.search(start_cursor=response.get('next_cursor'))
            all_results.extend(response.get('results', []))

        # Separate databases and pages
        for item in all_results:
            if item.get('object') == 'database':
                databases.append(item)
            elif item.get('object') == 'page':
                pages.append(item)

        # Show databases
        if filter_type in ['all', 'database']:
            print("\n📊 DATABASES")
            print("-" * 70)

            if databases:
                for i, db in enumerate(databases, 1):
                    title = ""
                    if db.get('title'):
                        title = db['title'][0].get('plain_text', 'Untitled') if db['title'] else 'Untitled'
                    db_id = db['id']
                    url = db.get('url', '')
                    props = list(db.get('properties', {}).keys())[:5]
                    print(f"\n{i}. {title}")
                    print(f"   ID: {db_id}")
                    print(f"   URL: {url}")
                    print(f"   Properties: {', '.join(props)}")
            else:
                print("   No databases accessible")

        # Show pages
        if filter_type in ['all', 'page']:
            print("\n\n📄 PAGES")
            print("-" * 70)

            if pages:
                for i, page in enumerate(pages, 1):
                    title = "Untitled"
                    if 'properties' in page:
                        for prop in page['properties'].values():
                            if prop.get('type') == 'title':
                                title_arr = prop.get('title', [])
                                if title_arr:
                                    title = title_arr[0].get('plain_text', 'Untitled')
                                    break
                    page_id = page['id']
                    url = page.get('url', '')
                    parent_type = page.get('parent', {}).get('type', 'unknown')
                    print(f"\n{i}. {title}")
                    print(f"   ID: {page_id}")
                    print(f"   URL: {url}")
                    print(f"   Parent: {parent_type}")
            else:
                print("   No pages accessible")

        print("\n" + "=" * 70)
        db_count = len(databases) if filter_type != 'page' else 0
        page_count = len(pages) if filter_type != 'database' else 0
        print(f"Total: {db_count} databases, {page_count} pages")
        print("=" * 70)

    except Exception as e:
        print(f"Error listing content: {e}")


def get_page_info(notion: Client, page_id: str) -> Optional[Dict]:
    """Get page information."""
    try:
        formatted_id = format_id_with_dashes(page_id)
        return notion.pages.retrieve(page_id=formatted_id)
    except Exception as e:
        print(f"Error retrieving page: {e}")
        return None


def get_database_info(notion: Client, target_id: str) -> Optional[Dict]:
    """
    Fetch full schema for a database or data source.

    Accepts either a database_id or a data_source_id; resolves to the
    data_source_id first, then returns the data source object (which contains
    the property schema under the 2025-09-03 API).
    """
    try:
        formatted_id = format_id_with_dashes(target_id)
        data_source_id = compat.resolve_data_source_id(notion, formatted_id)
        return compat.get_schema(notion, data_source_id)
    except APIResponseError as exc:
        print(f"Error retrieving database: {compat.explain_api_error(exc, formatted_id)}")
        return None
    except Exception as e:
        print(f"Error retrieving database: {e}")
        return None


def append_to_page(notion: Client, page_id: str, blocks: List[Dict]) -> bool:
    """Append blocks to an existing Notion page."""
    try:
        formatted_id = format_id_with_dashes(page_id)

        # Notion API limits to 100 blocks per request
        for i in range(0, len(blocks), 100):
            batch = blocks[i:i+100]
            notion.blocks.children.append(
                block_id=formatted_id,
                children=batch
            )

        return True
    except Exception as e:
        print(f"Error appending to page: {e}")
        return False


def clear_page_content(notion: Client, page_id: str) -> bool:
    """Clear all content from a page."""
    try:
        formatted_id = format_id_with_dashes(page_id)

        # Get all child blocks
        children = notion.blocks.children.list(block_id=formatted_id)

        # Delete each block (skip already archived blocks)
        for block in children.get('results', []):
            if block.get('archived'):
                continue
            notion.blocks.delete(block_id=block['id'])

        return True
    except Exception as e:
        print(f"Error clearing page: {e}")
        return False


def create_database_row(notion: Client, data_source_id: str, properties: Dict, content_blocks: List[Dict] = None) -> Optional[str]:
    """
    Create a new row in a Notion data source (multi-source database model).

    `data_source_id` must be a resolved data_source_id, not a database_id.
    Use compat.resolve_data_source_id() upstream.
    """
    try:
        page_data = {
            "parent": compat.build_data_source_parent(data_source_id),
            "properties": properties,
        }

        if content_blocks:
            page_data["children"] = content_blocks[:100]  # API limit on creation

        result = notion.pages.create(**page_data)

        # If more than 100 blocks, append the rest
        if content_blocks and len(content_blocks) > 100:
            append_to_page(notion, result['id'], content_blocks[100:])

        return result['id']
    except APIResponseError as exc:
        print(f"Error creating database row: {compat.explain_api_error(exc)}")
        return None
    except Exception as e:
        print(f"Error creating database row: {e}")
        return None


def update_page_properties(notion: Client, page_id: str, properties: Dict) -> bool:
    """Update properties on an existing page (used by upsert)."""
    try:
        notion.pages.update(page_id=page_id, properties=properties)
        return True
    except APIResponseError as exc:
        print(f"Error updating page properties: {compat.explain_api_error(exc)}")
        return False
    except Exception as e:
        print(f"Error updating page properties: {e}")
        return False


def write_to_page(notion: Client, page_id: str, markdown_content: str, mode: str = 'append') -> bool:
    """Write markdown content to a Notion page."""
    blocks = markdown_to_notion_blocks(markdown_content)

    if not blocks:
        print("No content to write")
        return False

    if mode == 'replace':
        if not clear_page_content(notion, page_id):
            return False

    return append_to_page(notion, page_id, blocks)


def main():
    parser = argparse.ArgumentParser(
        description='Push markdown content to Notion pages or databases',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
Examples:
  # Append markdown file to a page
  python notion_writer.py --page https://notion.so/My-Page-abc123 --file content.md

  # Replace page content
  python notion_writer.py --page PAGE_ID --file content.md --replace

  # Pipe content from stdin
  cat content.md | python notion_writer.py --page PAGE_ID --stdin

  # Create database row with title
  python notion_writer.py --database DB_URL --title "New Entry" --file content.md

  # Test connection
  python notion_writer.py --test
'''
    )

    parser.add_argument('--page', '-p', help='Notion page URL or ID')
    parser.add_argument('--database', '-d', help='Notion database URL, database_id, or data_source_id')
    parser.add_argument('--file', '-f', help='Markdown file to push')
    parser.add_argument('--stdin', action='store_true', help='Read content from stdin')
    parser.add_argument('--replace', '-r', action='store_true', help='Replace page content instead of append')
    parser.add_argument('--title', '-t', help='Title for database row (required for database unless --properties sets it)')
    parser.add_argument('--properties', help='Extra properties as JSON object literal or path to .json file (e.g. \'{"Status": "In progress", "Topic": ["AI"]}\')')
    parser.add_argument('--upsert-by', dest='upsert_by', help='Property name to match for upsert; updates existing row instead of creating duplicate')
    parser.add_argument('--test', action='store_true', help='Test Notion API connection')
    parser.add_argument('--list', '-l', nargs='?', const='all', choices=['all', 'pages', 'databases'],
                        help='List accessible pages and/or databases (default: all)')
    parser.add_argument('--info', action='store_true', help='Show page/database info')

    args = parser.parse_args()

    if not NOTION_TOKEN:
        print("Error: NOTION_API_KEY not set in .env file")
        print("Get your integration token from: https://www.notion.so/my-integrations")
        sys.exit(1)

    notion = compat.make_client(NOTION_TOKEN)

    # Test connection
    if args.test:
        try:
            me = notion.users.me()
            print("=" * 50)
            print("Notion API Connection Test")
            print("=" * 50)
            print(f"\n✅ Connected successfully!")
            print(f"   Bot: {me.get('name', 'Unknown')}")
            print(f"   Type: {me.get('type', 'Unknown')}")
            print("\n" + "=" * 50)
            return
        except Exception as e:
            print(f"❌ Connection failed: {e}")
            sys.exit(1)

    # List accessible content
    if args.list:
        filter_map = {'all': 'all', 'pages': 'page', 'databases': 'database'}
        list_accessible_content(notion, filter_map.get(args.list, 'all'))
        return

    # Show info
    if args.info:
        if args.page:
            page_id = extract_notion_id(args.page)
            if page_id:
                info = get_page_info(notion, page_id)
                if info:
                    print("=" * 50)
                    print("Page Information")
                    print("=" * 50)
                    title = ""
                    if 'properties' in info:
                        for prop in info['properties'].values():
                            if prop.get('type') == 'title':
                                title_arr = prop.get('title', [])
                                if title_arr:
                                    title = title_arr[0].get('plain_text', '')
                    print(f"Title: {title}")
                    print(f"ID: {info['id']}")
                    print(f"Created: {info.get('created_time', 'N/A')}")
                    print(f"URL: {info.get('url', 'N/A')}")
                    return

        if args.database:
            db_id = extract_notion_id(args.database)
            if db_id:
                info = get_database_info(notion, db_id)
                if info:
                    print("=" * 50)
                    print("Database Information")
                    print("=" * 50)
                    title = ""
                    if info.get('title'):
                        title = info['title'][0].get('plain_text', '') if info['title'] else ''
                    print(f"Title: {title}")
                    print(f"ID: {info['id']}")
                    print(f"Properties: {', '.join(info.get('properties', {}).keys())}")
                    return

        print("Please specify --page or --database with --info")
        return

    # Get content
    content = None
    if args.stdin:
        content = sys.stdin.read()
    elif args.file:
        file_path = Path(args.file)
        if not file_path.exists():
            print(f"Error: File not found: {args.file}")
            sys.exit(1)
        content = file_path.read_text(encoding='utf-8')

    # Write to page
    if args.page:
        if not content:
            print("Error: No content provided. Use --file or --stdin")
            sys.exit(1)

        page_id = extract_notion_id(args.page)
        if not page_id:
            print(f"Error: Invalid Notion page URL/ID: {args.page}")
            sys.exit(1)

        mode = 'replace' if args.replace else 'append'
        print(f"{'Replacing' if mode == 'replace' else 'Appending'} content to page...")

        if write_to_page(notion, page_id, content, mode):
            print(f"✅ Successfully wrote content to page")
            formatted_id = format_id_with_dashes(page_id)
            print(f"   https://notion.so/{formatted_id.replace('-', '')}")
        else:
            print("❌ Failed to write content")
            sys.exit(1)
        return

    # Create or upsert database row
    if args.database:
        if not content and not args.title and not args.properties:
            print("Error: Provide --title, --properties, and/or --file for database row")
            sys.exit(1)

        db_id = extract_notion_id(args.database)
        if not db_id:
            print(f"Error: Invalid Notion database URL/ID: {args.database}")
            sys.exit(1)
        formatted_id = format_id_with_dashes(db_id)

        # Resolve to a data_source_id (handles both database_id and data_source_id input)
        try:
            data_source_id = compat.resolve_data_source_id(notion, formatted_id)
        except APIResponseError as exc:
            print(f"Error: {compat.explain_api_error(exc, formatted_id)}")
            sys.exit(1)
        except ValueError as exc:
            print(f"Error: {exc}")
            sys.exit(1)

        # Fetch schema
        schema = compat.get_schema(notion, data_source_id)
        schema_props = schema.get('properties', {})
        title_prop = compat.find_title_property(schema_props)
        if not title_prop:
            print("Error: Data source has no title property; cannot create row.")
            sys.exit(1)

        # Build properties body: start from --properties, layer --title on top
        property_values: Dict[str, Any] = {}
        if args.properties:
            try:
                property_values = compat.parse_properties_arg(args.properties)
            except (ValueError, json.JSONDecodeError) as exc:
                print(f"Error parsing --properties: {exc}")
                sys.exit(1)

        if args.title:
            property_values[title_prop] = args.title
        elif title_prop not in property_values:
            property_values[title_prop] = "Untitled"

        try:
            properties = compat.coerce_properties(schema, property_values)
        except ValueError as exc:
            print(f"Error: {exc}")
            sys.exit(1)

        content_blocks = markdown_to_notion_blocks(content) if content else None

        # Upsert path: look for existing row by the named property
        if args.upsert_by:
            if args.upsert_by not in schema_props:
                valid = ", ".join(sorted(schema_props.keys()))
                print(f"Error: --upsert-by property {args.upsert_by!r} not in schema. Valid: {valid}")
                sys.exit(1)
            lookup_value = property_values.get(args.upsert_by)
            if lookup_value is None:
                print(f"Error: --upsert-by {args.upsert_by!r} requires a value in --properties or --title")
                sys.exit(1)

            try:
                existing = compat.find_existing_page(
                    notion, data_source_id, args.upsert_by,
                    schema_props[args.upsert_by], lookup_value,
                )
            except APIResponseError as exc:
                print(f"Error during upsert lookup: {compat.explain_api_error(exc)}")
                sys.exit(1)

            if existing:
                page_id = existing['id']
                print(f"Updating existing row (matched on {args.upsert_by}={lookup_value!r})...")
                if not update_page_properties(notion, page_id, properties):
                    sys.exit(1)
                if content_blocks:
                    if not clear_page_content(notion, page_id):
                        sys.exit(1)
                    if not append_to_page(notion, page_id, content_blocks):
                        sys.exit(1)
                print(f"✅ Successfully updated database row")
                print(f"   https://notion.so/{page_id.replace('-', '')}")
                return

        print(f"Creating database row...")
        row_id = create_database_row(notion, data_source_id, properties, content_blocks)

        if row_id:
            print(f"✅ Successfully created database row")
            print(f"   https://notion.so/{row_id.replace('-', '')}")
        else:
            print("❌ Failed to create database row")
            sys.exit(1)
        return

    # No action specified
    parser.print_help()


if __name__ == "__main__":
    main()
