import onepasswd
from onepasswd.onepasswd import PasswdGen
from onepasswd.config import Config
from onepasswd import crypto
from onepasswd.onepasswd import DB
from onepasswd.onepasswd import GitDB
from onepasswd.tools import jmerge, jdiff
from onepasswd import ltlog
import pyperclip
from pathlib import Path
import os
import json
import shlex
import time
import tempfile
import click
import logging

log = ltlog.getLogger('onepasswd.client')

DEF_CONFIG_PATH = os.path.join(Path.home(), ".onepasswd", "onepasswd.json")
DEF_DB_PATH = os.path.join(Path.home(), ".onepasswd", "db")
DEF_BACKUP_PATH = os.path.join(Path.home(), ".onepasswd", "backup")


@click.group()
@click.option('-d', '--debug', 'debug', default='INFO', help='log level')
@click.option('-c', '--config', 'config', default=DEF_CONFIG_PATH, help='config path')
@click.option('-p', '--print_passwd', 'print_passwd', is_flag=True,
              help='print password (do not copy from clipboard)')
@click.pass_context
def cli(ctx: click.Context, debug, config, print_passwd):
    ctx.ensure_object(dict)
    logging.getLogger().setLevel(debug)
    ctx.obj['config_path'] = config
    if os.path.exists(config):
        ctx.obj['config'] = Config(config)
    if not is_clipboard_available():
        log.warn('clipboard is not available')
    ctx.obj['print_passwd'] = print_passwd


@cli.command('version')
@click.pass_context
def version(ctx: click.Context):
    click.echo(onepasswd.get_version())


@cli.command('init')
@click.option('-r', '--repo', 'repo', help='github repo (owner/name), used to upload database to github')
@click.option('-t', '--token', 'token', help='github token ("github_xxx")')
@click.option('-e', '--encrypted-token', 'encrypted_token', help='github token (encrypted)')
@click.option('--input-token', 'input_token', is_flag=True, help='get token from stdin')
@click.option('--db', 'db', default=DEF_DB_PATH, show_default=True, help='local database path')
@click.option('--backup', 'backup', default=DEF_BACKUP_PATH, show_default=True, help='database backup path')
@click.option('--no-sync', 'no_sync', is_flag=True, help='skip initial sync from github')
@click.pass_context
def init(ctx: click.Context, repo, token, db, backup, input_token, encrypted_token, no_sync):
    """
    Initialize onepasswd with optional GitHub sync.

    Examples:
        # Basic init (local only)
        onepasswd init

        # Init with GitHub sync (auto-sync from remote)
        onepasswd init --repo owner/repo --token github_pat_xxx

        # Init with encrypted token
        onepasswd init --repo owner/repo --encrypted-token <encrypted>

        # Init with token from stdin
        onepasswd init --repo owner/repo --input-token
    """
    cfg_path = from_ctx(ctx, 'config_path')
    if os.path.exists(cfg_path):
        click.echo(f'already initialized. if you want to re-init, please remove {cfg_path}')
        exit(0)
    prepare_dir(cfg_path)
    prepare_dir(db)
    prepare_dir(backup)
    cfg = {
        'db': db,
        'backup': backup
    }
    # init auth info
    passwd = get_passwd(ctx, promt='master password', confirm=True,
                        from_clipboard=False, key='passwd')
    auth = crypto.generate_auth_info(passwd)
    cfg['auth_info'] = auth

    # GitHub configuration
    if repo and (token or input_token or encrypted_token):
        # init github
        cfg['repo'] = repo
        if input_token:
            token = get_passwd(ctx, promt='token')
        if not encrypted_token:
            encrypted_token = crypto.encrypt_passwd(token, passwd)
        cfg['token'] = encrypted_token

        # write config first so sync command can use it
        with open(cfg_path, "w") as fp:
            json.dump(cfg, fp, indent=2)

        # sync from github automatically
        if not no_sync:
            click.echo('syncing from github...')
            try:
                ctx.obj['config'] = Config(cfg_path)
                ctx.invoke(sync)
                click.echo("init finish! synced from github successfully")
            except Exception as e:
                click.echo(f"warning: sync failed: {e}")
                click.echo("init finish! you can manually sync later with 'onepasswd sync'")
        else:
            click.echo('github sync enabled but skipped (--no-sync)')
            click.echo("init finish! run 'onepasswd sync' to sync with github")
    else:
        click.echo('github sync not enabled')
        # write config
        with open(cfg_path, "w") as fp:
            json.dump(cfg, fp, indent=2)
        click.echo("init finish!")


@cli.command('sync')
@click.pass_context
def sync(ctx: click.Context):
    cfg = from_ctx(ctx, 'config')
    assert cfg is not None
    passwd = get_passwd(ctx, promt='master password', from_clipboard=False, key='passwd')
    token = crypto.decrypt_passwd(cfg['token'], passwd)
    git = GitDB(cfg['repo'], token)
    remote_sha = git.get_remote_hash('db')

    # Check if local db exists
    local_db_exists = os.path.exists(cfg['db'])

    if local_db_exists and git.hashfile(cfg['db']) == remote_sha:
        click.echo("local db is already updated")
        return

    # download remote db
    remote_db = git.pull('db')
    log.debug('get remote db +++ ' + str(remote_db))

    if not local_db_exists:
        # First time sync: just download the remote db
        click.echo("downloading database from github...")
        with open(cfg['db'], 'wb') as fp:
            fp.write(remote_db)
        click.echo("sync success")
    else:
        # Merge with existing local db
        with tempfile.TemporaryDirectory(prefix='onepasswd') as dir:
            tmp_file = os.path.join(dir, 'db')
            with open(tmp_file, 'wb') as fp:
                fp.write(remote_db)
            click.echo("> diff >>>")
            jdiff.diff(cfg['db'], tmp_file)
            click.echo("> merge >>>")
            jmerge.merge(cfg['db'], tmp_file)
        # push
        git.push(cfg['db'], 'db', remote_sha)
        click.echo("sync success")


@cli.command('init-command')
@click.option('--copy', 'copy_to_clipboard', is_flag=True, help='copy command to clipboard')
@click.pass_context
def init_command(ctx: click.Context, copy_to_clipboard):
    """
    Generate onepasswd init command for setting up on a new device.

    This command outputs the full init command with encrypted GitHub token
    that can be run on another device to sync from the same repository.
    """
    cfg = from_ctx(ctx, 'config')
    if not cfg:
        click.echo("✗ Config not found. Run 'onepasswd init' first.")
        exit(1)

    # Check if GitHub is configured
    if 'repo' not in cfg or 'token' not in cfg:
        click.echo("✗ GitHub sync not configured.")
        click.echo("  Run 'onepasswd init --repo owner/repo --token xxx' to enable GitHub sync.")
        exit(1)

    repo = cfg['repo']
    encrypted_token = cfg['token']

    # Generate the command
    command = f'onepasswd init --repo {repo} --encrypted-token {encrypted_token}'

    click.echo("=" * 60)
    click.echo("Init Command for New Device")
    click.echo("=" * 60)
    click.echo("\nRun this command on your new device:\n")
    click.echo(command)
    click.echo("\n" + "=" * 60)
    click.echo("\nNote: You'll need to enter your master password when running this command.")
    click.echo("The command will automatically sync your passwords from GitHub.")

    if copy_to_clipboard and is_clipboard_available():
        pyperclip.copy(command)
        click.echo("\n✓ Command copied to clipboard!")
    elif copy_to_clipboard:
        click.echo("\n✗ Clipboard not available, please copy manually.")


@cli.command('get')
@click.argument('keywords', nargs=-1)
@click.pass_context
def get(ctx: click.Context, keywords: tuple):
    if len(keywords) == 0:
        keywords = ('',)
    cfg = from_ctx(ctx, 'config')
    db = get_db(ctx)

    def process_passwd(passwd_enc):
        passwd = get_passwd(ctx, promt='master password', from_clipboard=False, key='passwd')
        if not crypto.auth(cfg['auth_info'], passwd):
            click.echo("password wrong")
            exit(1)
        give_passwd(ctx, crypto.decrypt_passwd(passwd_enc, passwd))

    # accurate match
    if keywords in db:
        item = db.get_item(keywords)
        click.echo(passwd_item_str(item))
        process_passwd(item['passwd'])
        return

    for v in db.find(keywords):
        if click.confirm(passwd_item_str(v)):
            process_passwd(v['passwd'])
            break


@cli.command('list')
@click.argument('keywords', nargs=-1)
@click.option('--plain', 'plain', is_flag=True, help='plain output without colors')
@click.option('--detail', 'show_detail', is_flag=True, help='show detailed information')
@click.pass_context
def list_cmd(ctx: click.Context, keywords: tuple, plain, show_detail):
    """List password entries with enhanced display."""
    if len(keywords) == 0:
        keywords = ('',)
    db = get_db(ctx)

    matches = [v for v in db.find(keywords)]
    if not matches:
        click.echo("No entries found")
        return

    # Show count if searching
    if keywords != ('',):
        click.echo(f"Found {len(matches)} entries:\n")

    for idx, v in enumerate(matches, 1):
        if show_detail:
            _show_entry_detail(v, plain=plain)
            if idx < len(matches):
                click.echo()  # Blank line between entries
        else:
            click.echo(passwd_item_str(v, use_color=not plain))


@cli.command('add')
@click.argument('entries', nargs=-1)
@click.option('-l', '--len', 'length', default=12, show_default=True, help='password length')
@click.option('-s', '--strength', 'strength', default='aA0$', show_default=True,
              help='password strength')
@click.option('--title', 'title', help='display title')
@click.option('--url', 'url', help='website URL')
@click.option('--username', 'username', help='username or email')
@click.option('--description', 'description', help='notes or description')
@click.option('--category', 'category', help='category/folder')
@click.option('--tags', 'tags', help='comma-separated tags')
@click.option('-i', '--interactive', 'interactive', is_flag=True, help='interactive mode with prompts')
@click.pass_context
def add(ctx: click.Context, entries: tuple, length, strength, title, url, username, 
        description, category, tags, interactive):
    d = {}

    def getpass():
        passwd = PasswdGen.generate(length, pre_process_strength(strength))
        d['passwd'] = passwd
        return passwd
    
    # Collect optional fields
    extra_fields = {}
    if interactive:
        # Interactive mode: prompt for all fields
        if not title:
            title = click.prompt('Title (display name)', default='', show_default=False)
        if not url:
            url = click.prompt('URL (website)', default='', show_default=False)
        if not username:
            username = click.prompt('Username/Email', default='', show_default=False)
        if not description:
            description = click.prompt('Description/Notes', default='', show_default=False)
        if not category:
            category = click.prompt('Category', default='', show_default=False)
        if not tags:
            tags = click.prompt('Tags (comma-separated)', default='', show_default=False)
    
    # Build extra fields dict
    if title:
        extra_fields['title'] = title
    if url:
        extra_fields['url'] = url
    if username:
        extra_fields['username'] = username
    if description:
        extra_fields['description'] = description
    if category:
        extra_fields['category'] = category
    if tags:
        extra_fields['tags'] = [t.strip() for t in tags.split(',')]
    
    if save_passwd(ctx, entries, getpass, **extra_fields):
        give_passwd(ctx, d['passwd'])
        if click.confirm('sync ?'):
            ctx.invoke(sync)


@cli.command('save')
@click.argument('entries', nargs=-1)
@click.option('--title', 'title', help='display title')
@click.option('--url', 'url', help='website URL')
@click.option('--username', 'username', help='username or email')
@click.option('--description', 'description', help='notes or description')
@click.option('--category', 'category', help='category/folder')
@click.option('--tags', 'tags', help='comma-separated tags')
@click.pass_context
def save(ctx: click.Context, entries: tuple, title, url, username, description, category, tags):
    def getpass():
        return get_passwd(ctx, promt='new password', confirm=True)
    
    # Build extra fields dict
    extra_fields = {}
    if title:
        extra_fields['title'] = title
    if url:
        extra_fields['url'] = url
    if username:
        extra_fields['username'] = username
    if description:
        extra_fields['description'] = description
    if category:
        extra_fields['category'] = category
    if tags:
        extra_fields['tags'] = [t.strip() for t in tags.split(',')]
    
    if save_passwd(ctx, entries, getpass, **extra_fields):
        if click.confirm('sync ?'):
            ctx.invoke(sync)


@cli.command('del')
@click.argument('keywords', nargs=-1)
@click.pass_context
def delete(ctx: click.Context, keywords: tuple):
    if len(keywords) == 0:
        keywords = ('',)
    db = get_db(ctx)
    for v in db.find(keywords):
        if click.confirm(passwd_item_str(v)):
            input = click.prompt('input entries to confirm delete')
            entries = shlex.split(input)
            if entries == v['entries']:
                del db[entries]
                click.echo(f'{passwd_item_str(v)} deleted')
                break


@cli.command('update')
@click.argument('keywords', nargs=-1)
@click.option('--title', 'title', help='update title')
@click.option('--url', 'url', help='update URL')
@click.option('--username', 'username', help='update username')
@click.option('--description', 'description', help='update description')
@click.option('--category', 'category', help='update category')
@click.option('--tags', 'tags', help='update tags (comma-separated)')
@click.pass_context
def update(ctx: click.Context, keywords: tuple, title, url, username, description, category, tags):
    """Update fields of an existing entry."""
    if len(keywords) == 0:
        keywords = ('',)
    db = get_db(ctx)

    # Find the entry
    matches = db.find(keywords)
    if not matches:
        click.echo("No entries found")
        return

    target = None
    if len(matches) == 1:
        target = matches[0]
    else:
        # Multiple matches, ask user to select
        for v in matches:
            if click.confirm(passwd_item_str(v)):
                target = v
                break

    if not target:
        return

    # Build update fields
    update_fields = {}
    if title is not None:
        update_fields['title'] = title
    if url is not None:
        update_fields['url'] = url
    if username is not None:
        update_fields['username'] = username
    if description is not None:
        update_fields['description'] = description
    if category is not None:
        update_fields['category'] = category
    if tags is not None:
        update_fields['tags'] = [t.strip() for t in tags.split(',')]

    if not update_fields:
        click.echo("No fields to update")
        return

    # Update the entry
    try:
        db.update_item(tuple(target['entries']), **update_fields)
        click.echo(f"Updated: {passwd_item_str(target)}")

        if click.confirm('sync ?'):
            ctx.invoke(sync)
    except KeyError as e:
        click.echo(f"Error: {e}")


@cli.command('edit')
@click.argument('keywords', nargs=-1)
@click.pass_context
def edit(ctx: click.Context, keywords: tuple):
    """Interactively edit all fields of an existing entry."""
    if len(keywords) == 0:
        keywords = ('',)
    db = get_db(ctx)
    cfg = from_ctx(ctx, 'config')

    # Find the entry
    matches = [v for v in db.find(keywords)]
    if not matches:
        click.echo("No entries found")
        return

    target = None
    if len(matches) == 1:
        target = matches[0]
        click.echo(f"Editing: {passwd_item_str(target)}")
    else:
        # Multiple matches, ask user to select
        click.echo(f"Found {len(matches)} entries:")
        for v in matches:
            if click.confirm(f"  {passwd_item_str(v)}"):
                target = v
                break

    if not target:
        return

    # Show current values
    click.echo("\nCurrent values (press Enter to keep current value):\n")

    # Edit title
    current_title = target.get('title', '')
    new_title = click.prompt('Title', default=current_title, show_default=True)

    # Edit URL
    current_url = target.get('url', '')
    new_url = click.prompt('URL', default=current_url, show_default=True)

    # Edit username
    current_username = target.get('username', '')
    new_username = click.prompt('Username/Email', default=current_username, show_default=True)

    # Edit description
    current_description = target.get('description', '')
    new_description = click.prompt('Description/Notes', default=current_description, show_default=True)

    # Edit category
    current_category = target.get('category', '')
    new_category = click.prompt('Category', default=current_category, show_default=True)

    # Edit tags
    current_tags = target.get('tags', [])
    current_tags_str = ', '.join(current_tags) if current_tags else ''
    new_tags_str = click.prompt('Tags (comma-separated)', default=current_tags_str, show_default=True)
    new_tags = [t.strip() for t in new_tags_str.split(',') if t.strip()]

    # Ask about password change
    change_password = click.confirm('\nChange password?', default=False)
    new_passwd_enc = None
    if change_password:
        passwd = get_passwd(ctx, promt='master password', from_clipboard=False, key='passwd')
        if not crypto.auth(cfg['auth_info'], passwd):
            click.echo("Master password wrong")
            return

        new_passwd_plain = click.prompt('New password', hide_input=True, confirmation_prompt=True)
        new_passwd_enc = crypto.encrypt_passwd(new_passwd_plain, passwd)

    # Build update fields
    update_fields = {
        'title': new_title,
        'url': new_url,
        'username': new_username,
        'description': new_description,
        'category': new_category,
        'tags': new_tags
    }

    # Show summary of changes
    click.echo("\n" + "=" * 60)
    click.echo("Summary of changes:")
    click.echo("=" * 60)
    changes_made = False

    for field, new_value in update_fields.items():
        old_value = target.get(field, '' if field != 'tags' else [])
        if field == 'tags':
            old_str = ', '.join(old_value) if old_value else '(empty)'
            new_str = ', '.join(new_value) if new_value else '(empty)'
            if old_value != new_value:
                click.echo(f"  {field}: {old_str} → {new_str}")
                changes_made = True
        else:
            if old_value != new_value:
                click.echo(f"  {field}: {old_value or '(empty)'} → {new_value or '(empty)'}")
                changes_made = True

    if change_password:
        click.echo("  password: (will be updated)")
        changes_made = True

    if not changes_made:
        click.echo("  (no changes)")
        return

    click.echo("=" * 60)

    # Confirm changes
    if not click.confirm('\nApply these changes?', default=True):
        click.echo("Cancelled.")
        return

    # Backup before updating
    db.backup(cfg['backup'])

    # Apply changes
    try:
        # Update metadata fields
        db.update_item(tuple(target['entries']), **update_fields)

        # Update password if changed
        if change_password and new_passwd_enc:
            entry_key = tuple(target['entries'])
            item = db.get_item(entry_key)
            item['passwd'] = new_passwd_enc
            db.db[entry_key] = item
            db.save()

        click.echo(f"\n✓ Updated: {passwd_item_str(target)}")

        if click.confirm('\nSync to GitHub?', default=False):
            ctx.invoke(sync)
    except Exception as e:
        click.echo(f"\n✗ Error: {e}")


@cli.command('show')
@click.argument('keywords', nargs=-1)
@click.option('--plain', 'plain', is_flag=True, help='plain output without colors')
@click.pass_context
def show(ctx: click.Context, keywords: tuple, plain):
    """Show detailed entry information."""
    if len(keywords) == 0:
        keywords = ('',)
    db = get_db(ctx)

    matches = [v for v in db.find(keywords)]
    if not matches:
        click.echo("No entries found")
        return

    for idx, v in enumerate(matches, 1):
        _show_entry_detail(v, plain=plain)

        if idx < len(matches):
            if not click.confirm("\nShow next entry?", default=True):
                break
            click.echo()  # Blank line


@cli.command('export')
@click.argument('format', type=click.Choice(['chrome', 'apple', 'json', 'text']))
@click.option('-o', '--output', 'output', required=True, help='output file path')
@click.pass_context
def export_cmd(ctx: click.Context, format, output):
    """Export passwords to Chrome/Apple/JSON/Text format."""
    cfg = from_ctx(ctx, 'config')
    db = get_db(ctx)
    passwd = get_passwd(ctx, promt='master password', from_clipboard=False, key='passwd')
    
    if not crypto.auth(cfg['auth_info'], passwd):
        click.echo("password wrong")
        exit(1)
    
    from onepasswd.exporters import get_exporter
    exporter = get_exporter(format)
    
    try:
        exporter.export(db.get_all_items(), output, passwd)
        click.echo(f"✓ Exported to {output}")
        click.echo(f"✓ Format: {format}")
        
        # Show warning for plain text formats
        if format in ['chrome', 'apple', 'text']:
            click.echo("")
            click.echo("⚠️  WARNING: This file contains UNENCRYPTED passwords!")
            click.echo("   Please keep it secure and delete it after importing.")
    except Exception as e:
        click.echo(f"✗ Export failed: {e}")
        exit(1)


@cli.command('dec')
@click.argument('cipher')
@click.pass_context
def dec(ctx: click.Context, cipher):
    passwd = get_passwd(ctx, from_clipboard=False)
    text = crypto.decrypt_passwd(cipher, passwd)
    give_passwd(ctx, text)


@cli.command('enc')
@click.option('-d', '--data', 'data')
@click.pass_context
def enc(ctx: click.Context, data):
    passwd = get_passwd(ctx, from_clipboard=False)
    if not data:
        data = get_passwd(ctx, promt='content to encrypt')
    cipher = crypto.encrypt_passwd(data, passwd)
    give_passwd(ctx, cipher)


@cli.command('web')
@click.option('-h', '--host', 'host', default='0.0.0.0')
@click.option('-p', '--port', 'port', default=10066)
@click.option('-d', '--debug', 'debug', is_flag=True, default=False)
@click.pass_context
def web(ctx, host, port, debug):
    try:
        from onepasswd import web
    except ImportError:
        click.echo('you must install flask if you want to use onepasswd web')
        click.echo('try: pip install flask')
        exit(1)
    web.init(from_ctx(ctx, 'config'))
    web.app.run(host=host, port=port, debug=debug)


@cli.command('migrate')
@click.option('-i', '--interactive', 'interactive', is_flag=True, 
              help='interactive mode: prompt for each entry')
@click.option('--dry-run', 'dry_run', is_flag=True, help='preview changes without saving')
@click.pass_context
def migrate_cmd(ctx, interactive, dry_run):
    """
    Migrate old entries to v2 format with rich metadata.
    
    This tool helps you upgrade entries by adding metadata like title, URL,
    username, description, etc. Can auto-infer from keywords or prompt interactively.
    """
    from onepasswd.models import PasswordEntry
    
    cfg = from_ctx(ctx, 'config')
    if not cfg:
        click.echo("✗ Config not found. Run 'onepasswd init' first.")
        exit(1)
    
    db = get_db(ctx)
    
    click.echo("=" * 60)
    click.echo("onepasswd Migration Tool")
    click.echo("=" * 60)
    
    # Get master password
    passwd = get_passwd(ctx, promt='master password', from_clipboard=False, key='passwd')
    if not crypto.auth(cfg['auth_info'], passwd):
        click.echo("✗ Password wrong")
        exit(1)
    
    # Collect all entries
    entries_to_migrate = []
    entries_already_v2 = []
    
    for key, data in db.db.items():
        if 'deleted' in data:
            continue
        
        version = data.get('version', 1)
        entry = PasswordEntry.from_dict(data)
        
        # Migrate if v1 or missing metadata
        if version == 1 or not (entry.title or entry.url or entry.username):
            entries_to_migrate.append(entry)
        else:
            entries_already_v2.append(entry)
    
    click.echo(f"\nFound {len(entries_to_migrate)} entries to migrate")
    click.echo(f"Found {len(entries_already_v2)} entries already with metadata")
    
    if not entries_to_migrate:
        click.echo("\n✓ All entries are already up to date!")
        return
    
    if dry_run:
        click.echo("\n📋 DRY RUN MODE - No changes will be saved\n")
    
    click.echo("\n" + "=" * 60)
    
    updated_count = 0
    
    for idx, entry in enumerate(entries_to_migrate, 1):
        click.echo(f"\n[{idx}/{len(entries_to_migrate)}]")
        click.echo(f"Keywords: {' '.join(entry.entries)}")
        
        # Infer metadata
        inferred_title, inferred_url = entry.infer_metadata()
        
        if not interactive:
            # Auto mode: use inferred values
            title = entry.title or inferred_title
            url = entry.url or inferred_url
            username = entry.username or ""
            description = entry.description or ""
            category = entry.category or ""
            tags = entry.tags or []
            
            click.echo(f"  Auto-inferred title: {title}")
            click.echo(f"  Auto-inferred URL: {url}")
        else:
            # Interactive mode: prompt for each field
            click.echo(f"  Inferred title: {inferred_title}")
            click.echo(f"  Inferred URL: {inferred_url}")
            
            title = click.prompt('Title', default=inferred_title, show_default=True)
            url = click.prompt('URL', default=inferred_url, show_default=True)
            username = click.prompt('Username/Email', default='', show_default=False)
            description = click.prompt('Description', default='', show_default=False)
            category = click.prompt('Category', default='', show_default=False)
            tags_str = click.prompt('Tags (comma-separated)', default='', show_default=False)
            tags = [t.strip() for t in tags_str.split(',') if t.strip()]
        
        # Update entry
        update_fields = {}
        if title:
            update_fields['title'] = title
        if url:
            update_fields['url'] = url
        if username:
            update_fields['username'] = username
        if description:
            update_fields['description'] = description
        if category:
            update_fields['category'] = category
        if tags:
            update_fields['tags'] = tags
        
        if not dry_run and update_fields:
            try:
                db.update_item(tuple(entry.entries), **update_fields)
                updated_count += 1
                click.echo("  ✓ Updated")
            except Exception as e:
                click.echo(f"  ✗ Error: {e}")
        elif dry_run:
            click.echo("  ○ Would update (dry run)")
            updated_count += 1
        
        # Ask to continue in interactive mode
        if interactive and idx < len(entries_to_migrate):
            if not click.confirm("\nContinue to next entry?", default=True):
                break
    
    click.echo("\n" + "=" * 60)
    if dry_run:
        click.echo(f"✓ Dry run complete: {updated_count} entries would be updated")
        click.echo("\nRun without --dry-run to apply changes")
    else:
        click.echo(f"✓ Migration complete: {updated_count} entries updated")
    click.echo("=" * 60)


@cli.command('ai-migrate')
@click.option('--context', 'user_context', default='', help='Additional context for AI (e.g., "work accounts", "personal")')
@click.option('--model', 'model', default='gpt-4o-mini', help='OpenAI model to use')
@click.option('--api-key', 'api_key', help='OpenAI API key (or set OPENAI_API_KEY env var)')
@click.option('--batch-size', 'batch_size', default=20, help='Number of entries per API call')
@click.option('--max-concurrent', 'max_concurrent', default=3, help='Maximum concurrent API requests')
@click.option('--output', 'output_file', help='Output review file path')
@click.pass_context
def ai_migrate_generate(ctx: click.Context, user_context, model, api_key, batch_size, max_concurrent, output_file):
    """
    AI-powered migration: generate review file with AI-filled metadata.

    This command:
    1. Reads all entries with missing fields
    2. Uses OpenAI to intelligently fill missing metadata (concurrently)
    3. Generates a review file for you to approve
    4. Shows results in real-time as batches complete

    Examples:
        # Generate review file
        onepasswd ai-migrate --context "These are my personal accounts"

        # Use specific model with lower concurrency for rate limits
        onepasswd ai-migrate --model gpt-4o --max-concurrent 2

        # Specify output file
        onepasswd ai-migrate --output my_review.json

    After reviewing the file, apply changes with:
        onepasswd ai-migrate-apply <review_file>
    """
    from onepasswd.ai_migrate import AIPasswordMigrator, check_openai_available
    import asyncio

    if not check_openai_available():
        click.echo("✗ Error: OpenAI SDK not installed")
        click.echo("Install with: pip install openai")
        exit(1)

    # Check if config exists
    if 'config' not in ctx.obj:
        click.echo("✗ Error: onepasswd not initialized")
        click.echo("Run: onepasswd init")
        exit(1)

    cfg = ctx.obj['config']
    db = get_db(ctx)

    # Get master password for authentication
    passwd = get_passwd(ctx, promt='master password', from_clipboard=False)
    if not crypto.auth(cfg['auth_info'], passwd):
        click.echo("✗ Password wrong")
        exit(1)

    click.echo("=" * 60)
    click.echo("AI-Powered Password Migration")
    click.echo("=" * 60)

    # Track results as they come in
    all_batch_results = []
    completed_batches = 0

    def progress_callback(batch_num, total_batches, batch_results):
        nonlocal completed_batches, all_batch_results
        completed_batches = batch_num
        all_batch_results.extend(batch_results)

        # Show sample results from this batch
        click.echo(f"\n[Batch {batch_num}/{total_batches}] ✓ Completed - {len(batch_results)} entries processed")

        # Show first result as preview
        if batch_results:
            sample = batch_results[0]
            keywords = ', '.join(sample['proposed'].get('keywords', sample['original'].get('keywords', [])))
            title = sample['proposed'].get('title', '(no title)')
            url = sample['proposed'].get('url', '(no url)')
            confidence = sample['proposed'].get('confidence', 'unknown')

            click.echo(f"  Sample: [{keywords}]")
            click.echo(f"    → {title}")
            click.echo(f"    → {url}")
            click.echo(f"    → Confidence: {confidence}")

    try:
        # Initialize AI migrator
        click.echo(f"\n🤖 Initializing AI with model: {model}")
        click.echo(f"⚙️  Max concurrent requests: {max_concurrent}")
        migrator = AIPasswordMigrator(api_key=api_key, model=model)

        # Process entries with real-time progress
        click.echo("📖 Reading entries from database...")
        click.echo("⚡ Processing batches with rate limiting...\n")

        # Run async processing with progress callback
        results = asyncio.run(
            migrator.process_entries_async(
                db,
                user_context=user_context,
                batch_size=batch_size,
                max_concurrent=max_concurrent,
                progress_callback=progress_callback
            )
        )

        if not results.get('entries'):
            click.echo("\n✓ No entries need migration! All entries have complete metadata.")
            return

        click.echo(f"\n{'=' * 60}")
        click.echo(f"✓ All batches complete! Processed {results['total_processed']} entries")
        click.echo(f"{'=' * 60}")

        # Generate review file
        click.echo("\n📝 Generating review file...")
        review_path = migrator.generate_review_file(results, output_path=output_file)

        click.echo("\n" + "=" * 60)
        click.echo("✓ Review file generated!")
        click.echo("=" * 60)
        click.echo(f"\nFile: {review_path}")
        click.echo(f"Entries processed: {results['total_processed']}")
        click.echo("\nNext steps:")
        click.echo("1. Review and edit the file (modify 'proposed' fields as needed)")
        click.echo("2. Set 'approved: false' for entries you want to skip")
        click.echo("3. Apply changes with:")
        click.echo(f"\n   onepasswd ai-migrate-apply {review_path}\n")

    except ImportError as e:
        click.echo(f"\n✗ Error: {e}")
        exit(1)
    except ValueError as e:
        click.echo(f"\n✗ Configuration error: {e}")
        exit(1)
    except Exception as e:
        click.echo(f"\n✗ Error during AI migration: {e}")
        log.exception(e)
        exit(1)


@cli.command('ai-migrate-apply')
@click.argument('review_file', type=click.Path(exists=True))
@click.option('--dry-run', 'dry_run', is_flag=True, help='Preview changes without applying')
@click.pass_context
def ai_migrate_apply(ctx: click.Context, review_file, dry_run):
    """
    Apply approved changes from AI migration review file.

    Args:
        review_file: Path to the review JSON file

    Examples:
        # Preview changes
        onepasswd ai-migrate-apply review.json --dry-run

        # Apply changes
        onepasswd ai-migrate-apply review.json
    """
    from onepasswd.ai_migrate import AIPasswordMigrator, check_openai_available
    import json

    if not check_openai_available():
        click.echo("✗ Error: OpenAI SDK not installed")
        click.echo("Install with: pip install openai")
        exit(1)

    # Check if config exists
    if 'config' not in ctx.obj:
        click.echo("✗ Error: onepasswd not initialized")
        click.echo("Run: onepasswd init")
        exit(1)

    cfg = ctx.obj['config']
    db = get_db(ctx)

    # Get master password for authentication
    passwd = get_passwd(ctx, promt='master password', from_clipboard=False)
    if not crypto.auth(cfg['auth_info'], passwd):
        click.echo("✗ Password wrong")
        exit(1)

    click.echo("=" * 60)
    click.echo("Apply AI Migration Changes")
    click.echo("=" * 60)

    # Load and preview review file
    try:
        with open(review_file, 'r') as f:
            review_data = json.load(f)
    except Exception as e:
        click.echo(f"\n✗ Error reading review file: {e}")
        exit(1)

    total_entries = len(review_data.get('entries', []))
    approved_entries = sum(1 for e in review_data.get('entries', []) if e.get('approved', True))

    click.echo(f"\nReview file: {review_file}")
    click.echo(f"Total entries: {total_entries}")
    click.echo(f"Approved entries: {approved_entries}")
    click.echo(f"Skipped entries: {total_entries - approved_entries}")

    if dry_run:
        click.echo("\n📋 DRY RUN MODE - No changes will be applied\n")

    # Show preview of changes
    click.echo("\n" + "-" * 60)
    click.echo("Preview of changes:")
    click.echo("-" * 60)

    for idx, entry in enumerate(review_data.get('entries', [])[:5], 1):  # Show first 5
        if not entry.get('approved', True):
            continue

        click.echo(f"\n[{idx}] Keywords: {', '.join(entry['keywords'])}")
        proposed = entry.get('proposed', {})

        for field in ['title', 'url', 'username', 'category']:
            value = proposed.get(field)
            if value:
                click.echo(f"    {field}: {value}")

    if approved_entries > 5:
        click.echo(f"\n... and {approved_entries - 5} more entries")

    click.echo("\n" + "-" * 60)

    if not dry_run:
        if not click.confirm("\nApply these changes to the database?", default=True):
            click.echo("Cancelled.")
            return

        # Backup before applying
        click.echo("\n💾 Creating backup...")
        db.backup(cfg['backup'])

    # Apply changes
    try:
        migrator = AIPasswordMigrator()

        if dry_run:
            click.echo("\n✓ Dry run complete (no changes applied)")
        else:
            click.echo("\n⚙️  Applying changes...")
            result = migrator.apply_review_file(db, review_file, passwd)

            click.echo("\n" + "=" * 60)
            click.echo("✓ Migration complete!")
            click.echo("=" * 60)
            click.echo(f"Applied: {result['applied']}")
            click.echo(f"Skipped: {result['skipped']}")
            click.echo(f"Errors: {result['errors']}")

            if result['errors'] > 0:
                click.echo("\n⚠️  Some entries failed to update. Check logs for details.")

    except Exception as e:
        click.echo(f"\n✗ Error applying changes: {e}")
        log.exception(e)
        exit(1)


def save_passwd(ctx, entries, getpass, **extra_fields):
    """
    Save password with optional extra fields.
    
    Args:
        ctx: Click context
        entries: Entry keywords tuple
        getpass: Function to get the password
        **extra_fields: Optional fields (title, url, username, etc.)
    """
    assert len(entries) >= 1
    db = get_db(ctx)
    if entries in db:
        item = db.get_item(entries)
        if not click.confirm(
                f'{passwd_item_str(item)} already in database, update ?'):
            return False
        cfg = from_ctx(ctx, 'config')
        db.backup(cfg['backup'])
    
    passwd = get_passwd(ctx, promt='master password', from_clipboard=False, key='passwd')
    passwd_dec = getpass()
    passwd_enc = crypto.encrypt_passwd(passwd_dec, passwd)
    
    # Build entry data with extra fields
    if extra_fields:
        entry_data = {
            'passwd': passwd_enc,
            'time': str(time.time()),
            **extra_fields
        }
        db[entries] = entry_data
    else:
        # Simple mode: just password
        db[entries] = passwd_enc
    
    return True


def pre_process_strength(s):
    strength = set()
    for x in s:
        for i, t in enumerate(PasswdGen.passwd_table):
            if x in t:
                strength.add(i)
                break
    return strength


def passwd_item_str(item, use_color=True):
    """
    Format password entry for display.

    Args:
        item: Password entry dict
        use_color: Whether to use ANSI color codes

    Returns:
        Formatted string
    """
    from colored import fg, attr

    # Extract data
    entries = item.get('entries', [])
    title = item.get('title', '')
    url = item.get('url', '')
    username = item.get('username', '')
    category = item.get('category', '')
    tags = item.get('tags', [])
    version = item.get('version', 1)

    # Primary identifier (keywords or title)
    if title:
        primary = title
        secondary = ' '.join([f'"{x}"' for x in entries])
    else:
        primary = ' '.join([f'"{x}"' for x in entries])
        secondary = None

    # Build display parts
    parts = []

    if use_color:
        # Colored output
        parts.append(f"{fg('cyan')}{primary}{attr('reset')}")
        if secondary:
            parts.append(f"{fg('white')}({secondary}){attr('reset')}")
        if url:
            parts.append(f"{fg('blue')}{url}{attr('reset')}")
        if username:
            parts.append(f"{fg('yellow')}\u2022 {username}{attr('reset')}")
        if category:
            parts.append(f"{fg('green')}[{category}]{attr('reset')}")
        if tags:
            tags_str = ' '.join([f"#{tag}" for tag in tags])
            parts.append(f"{fg('magenta')}{tags_str}{attr('reset')}")
    else:
        # Plain output
        parts.append(primary)
        if secondary:
            parts.append(f"({secondary})")
        if url:
            parts.append(f"\u2192 {url}")
        if username:
            parts.append(f"\u2022 {username}")
        if category:
            parts.append(f"[{category}]")
        if tags:
            tags_str = ' '.join([f"#{tag}" for tag in tags])
            parts.append(tags_str)

    return ' '.join(parts)


def _show_entry_detail(item, plain=False):
    """
    Display detailed entry information.

    Args:
        item: Password entry dict
        plain: Whether to use plain output (no colors)
    """
    from colored import fg, attr

    def label(text, color='white'):
        if plain:
            return f"{text}:"
        return f"{fg(color)}{text}:{attr('reset')}"

    def value(text, color='white', bold=False):
        if plain or not text:
            return text if text else "(not set)"
        style = attr('bold') if bold else ''
        return f"{style}{fg(color)}{text}{attr('reset')}"

    # Header
    if not plain:
        click.echo(f"{fg('cyan')}{'─' * 60}{attr('reset')}")

    # Title
    title = item.get('title', '')
    entries = ' '.join([f'"{x}"' for x in item.get('entries', [])])
    if title:
        click.echo(f"{label('Title', 'cyan')} {value(title, 'cyan', bold=True)}")
        click.echo(f"{label('Keywords', 'white')} {value(entries, 'white')}")
    else:
        click.echo(f"{label('Keywords', 'cyan')} {value(entries, 'cyan', bold=True)}")

    # URL
    url = item.get('url', '')
    if url:
        click.echo(f"{label('URL', 'blue')} {value(url, 'blue')}")

    # Username
    username = item.get('username', '')
    if username:
        click.echo(f"{label('Username', 'yellow')} {value(username, 'yellow')}")

    # Category
    category = item.get('category', '')
    if category:
        click.echo(f"{label('Category', 'green')} {value(category, 'green')}")

    # Tags
    tags = item.get('tags', [])
    if tags:
        tags_str = ', '.join([f"#{tag}" for tag in tags])
        click.echo(f"{label('Tags', 'magenta')} {value(tags_str, 'magenta')}")

    # Description
    description = item.get('description', '')
    if description:
        click.echo(f"{label('Description', 'white')} {value(description, 'white')}")

    # Timestamps
    created_time = item.get('created_time', item.get('time'))
    modified_time = item.get('modified_time', item.get('time'))
    if created_time:
        created_str = time.ctime(float(created_time))
        click.echo(f"{label('Created', 'white')} {value(created_str, 'white')}")
    if modified_time and modified_time != created_time:
        modified_str = time.ctime(float(modified_time))
        click.echo(f"{label('Modified', 'white')} {value(modified_str, 'white')}")

    # Footer
    if not plain:
        click.echo(f"{fg('cyan')}{'─' * 60}{attr('reset')}")


def get_db(ctx: click.Context):
    config = from_ctx(ctx, 'config')
    return DB(config['db'])


def from_ctx(ctx: click.Context, name, default=None):
    return ctx.obj.get(name, default)


def check_config(cfg: dict):
    keys = ['db', 'backup']
    for key in keys:
        if key not in cfg:
            return False
    return True


def prepare_dir(filepath):
    dir = os.path.dirname(filepath)
    if not os.path.exists(dir):
        os.makedirs(dir)


def get_passwd(ctx: click.Context = None, promt="password", from_clipboard=None, confirm=False, key=None):
    def ret_password(passwd):
        if ctx and key and key not in ctx.obj:
            ctx.obj[key] = passwd
        return passwd
    if ctx and key and key in ctx.obj:
        return from_ctx(ctx, key)
    if (from_clipboard is None and click.confirm(f'[{promt}] use input from clipboard ?')) \
            or from_clipboard:
        passwd = pyperclip.paste().strip()
        if len(passwd) == 0:
            click.echo('your clipboard contains northing')
        else:
            return ret_password(passwd)
    passwd = click.prompt(f'please input {promt}', hide_input=True)
    passwd = passwd.strip()
    if confirm:
        confirm = click.prompt(f'confirm {promt}', hide_input=True)
        confirm = confirm.strip()
        if confirm != passwd:
            return get_passwd(ctx, promt=promt, from_clipboard=False, confirm=True, key=key)
    return ret_password(passwd)


def give_passwd(ctx: click.Context, passwd, no_echo=False):
    if not from_ctx(ctx, 'print_passwd') and is_clipboard_available():
        pyperclip.copy(passwd)
    else:
        if not no_echo:
            click.echo(passwd)


def is_clipboard_available():
    try:
        pyperclip.paste()
        return True
    except Exception as e:
        log.error(e)
        return False


def main():
    cli()


if __name__ == "__main__":
    main()
