# Generated by hand on 2026-01-01
# Copies ArchiveResult cmd/pwd/cmd_version data to Process records before removing old fields

from django.db import migrations, connection
import json
from pathlib import Path
from archivebox.uuid_compat import uuid7


def parse_cmd_field(cmd_raw):
    """
    Parse cmd field which could be:
    1. JSON array string: '["wget", "-p", "url"]'
    2. Space-separated string: 'wget -p url'
    3. NULL/empty

    Returns list of strings.
    """
    if not cmd_raw:
        return []

    cmd_raw = cmd_raw.strip()

    if not cmd_raw:
        return []

    # Try to parse as JSON first
    if cmd_raw.startswith('['):
        try:
            parsed = json.loads(cmd_raw)
            if isinstance(parsed, list):
                return [str(x) for x in parsed]
        except json.JSONDecodeError:
            pass

    # Fallback: split by spaces (simple approach, doesn't handle quoted strings)
    # This is acceptable since old cmd fields were mostly simple commands
    return cmd_raw.split()


def get_or_create_current_machine(cursor):
    """Get or create Machine.current() using raw SQL."""
    import socket
    from datetime import datetime

    # Simple machine detection - get hostname as guid
    hostname = socket.gethostname()
    guid = f'host_{hostname}'  # Simple but stable identifier

    # Check if machine exists
    cursor.execute("SELECT id FROM machine_machine WHERE guid = ?", [guid])
    row = cursor.fetchone()

    if row:
        return row[0]

    # Create new machine
    # Django UUIDField stores UUIDs as 32-char hex (no dashes) in SQLite
    machine_id = uuid7().hex
    now = datetime.now().isoformat()

    # Check which columns exist (schema differs between 0.8.x and 0.9.x)
    cursor.execute("PRAGMA table_info(machine_machine)")
    machine_cols = {row[1] for row in cursor.fetchall()}

    # Build INSERT statement based on available columns
    if 'config' in machine_cols:
        # 0.9.x schema with config column
        cursor.execute("""
            INSERT INTO machine_machine (
                id, created_at, modified_at, guid, hostname,
                hw_in_docker, hw_in_vm, hw_manufacturer, hw_product, hw_uuid,
                os_arch, os_family, os_platform, os_release, os_kernel,
                stats, config, num_uses_failed, num_uses_succeeded
            ) VALUES (?, ?, ?, ?, ?, 0, 0, '', '', '',
                      '', '', '', '', '', '{}', '{}', 0, 0)
        """, [machine_id, now, now, guid, hostname])
    else:
        # 0.8.x schema without config column
        cursor.execute("""
            INSERT INTO machine_machine (
                id, created_at, modified_at, guid, hostname,
                hw_in_docker, hw_in_vm, hw_manufacturer, hw_product, hw_uuid,
                os_arch, os_family, os_platform, os_release, os_kernel,
                stats, num_uses_failed, num_uses_succeeded
            ) VALUES (?, ?, ?, ?, ?, 0, 0, '', '', '',
                      '', '', '', '', '', '{}', 0, 0)
        """, [machine_id, now, now, guid, hostname])

    return machine_id


def get_or_create_binary(cursor, machine_id, name, abspath, version):
    """
    Get or create Binary record.

    Args:
        cursor: DB cursor
        machine_id: Machine FK
        name: Binary name (basename of command)
        abspath: Absolute path to binary (or just name if path unknown)
        version: Version string

    Returns:
        binary_id (str)
    """
    from datetime import datetime

    # If abspath is just a name without slashes, it's not a full path
    # Store it in both fields for simplicity
    if '/' not in abspath:
        # Not a full path - store as-is
        pass

    # Check if binary exists with same machine, name, abspath, version
    cursor.execute("""
        SELECT id FROM machine_binary
        WHERE machine_id = ? AND name = ? AND abspath = ? AND version = ?
    """, [machine_id, name, abspath, version])

    row = cursor.fetchone()
    if row:
        return row[0]

    # Create new binary
    # Django UUIDField stores UUIDs as 32-char hex (no dashes) in SQLite
    binary_id = uuid7().hex
    now = datetime.now().isoformat()

    # Check which columns exist (schema differs between 0.8.x and 0.9.x)
    cursor.execute("PRAGMA table_info(machine_binary)")
    binary_cols = {row[1] for row in cursor.fetchall()}

    # Use only columns that exist in current schema
    # 0.8.x schema: id, created_at, modified_at, machine_id, name, binprovider, abspath, version, sha256, num_uses_failed, num_uses_succeeded
    # 0.9.x schema adds: binproviders, overrides, status, retry_at, output_dir
    if 'binproviders' in binary_cols:
        # 0.9.x schema
        cursor.execute("""
            INSERT INTO machine_binary (
                id, created_at, modified_at, machine_id,
                name, binproviders, overrides, binprovider, abspath, version, sha256,
                status, retry_at, output_dir,
                num_uses_failed, num_uses_succeeded
            ) VALUES (?, ?, ?, ?, ?, 'env', '{}', 'env', ?, ?, '',
                      'succeeded', NULL, '', 0, 0)
        """, [binary_id, now, now, machine_id, name, abspath, version])
    else:
        # 0.8.x schema (simpler)
        cursor.execute("""
            INSERT INTO machine_binary (
                id, created_at, modified_at, machine_id,
                name, binprovider, abspath, version, sha256,
                num_uses_failed, num_uses_succeeded
            ) VALUES (?, ?, ?, ?, ?, 'env', ?, ?, '', 0, 0)
        """, [binary_id, now, now, machine_id, name, abspath, version])

    return binary_id


def map_status(old_status):
    """
    Map old ArchiveResult status to Process status and exit_code.

    Args:
        old_status: One of: queued, started, backoff, succeeded, failed, skipped

    Returns:
        (process_status, exit_code) tuple
    """
    status_map = {
        'queued': ('queued', None),
        'started': ('running', None),
        'backoff': ('queued', None),
        'succeeded': ('exited', 0),
        'failed': ('exited', 1),
        'skipped': ('exited', None),  # Skipped = exited without error
    }

    return status_map.get(old_status, ('queued', None))


def create_process(cursor, machine_id, pwd, cmd, status, exit_code, started_at, ended_at, binary_id):
    """
    Create a Process record.

    Returns:
        process_id (str)
    """
    from datetime import datetime

    # Django UUIDField stores UUIDs as 32-char hex (no dashes) in SQLite
    process_id = uuid7().hex
    now = datetime.now().isoformat()

    # Convert cmd array to JSON
    cmd_json = json.dumps(cmd)

    # Set retry_at to now for queued processes, NULL otherwise
    retry_at = now if status == 'queued' else None

    cursor.execute("""
        INSERT INTO machine_process (
            id, created_at, modified_at, machine_id, parent_id, process_type,
            pwd, cmd, env, timeout,
            pid, exit_code, stdout, stderr,
            started_at, ended_at,
            binary_id, iface_id, url,
            status, retry_at
        ) VALUES (?, ?, ?, ?, NULL, 'cli',
                  ?, ?, '{}', 120,
                  NULL, ?, '', '',
                  ?, ?,
                  ?, NULL, NULL,
                  ?, ?)
    """, [
        process_id, now, now, machine_id,
        pwd, cmd_json,
        exit_code,
        started_at, ended_at,
        binary_id,
        status, retry_at
    ])

    return process_id


def copy_archiveresult_data_to_process(apps, schema_editor):
    """
    Copy old ArchiveResult execution data (cmd, pwd, cmd_version) to Process records.

    For each ArchiveResult without a process_id:
    1. Parse cmd field (handle both JSON array and space-separated string)
    2. Extract binary name/path from cmd[0]
    3. Get or create Binary record with machine, name, abspath, version
    4. Create Process record with mapped fields
    5. Link ArchiveResult.process_id to new Process

    Status mapping:
    - queued → queued (exit_code=None)
    - started → running (exit_code=None)
    - backoff → queued (exit_code=None)
    - succeeded → exited (exit_code=0)
    - failed → exited (exit_code=1)
    - skipped → exited (exit_code=None)
    """
    cursor = connection.cursor()

    # Check if old fields still exist (skip if fresh install or already migrated)
    cursor.execute("PRAGMA table_info(core_archiveresult)")
    cols = {row[1] for row in cursor.fetchall()}

    print(f'DEBUG 0027: Columns found: {sorted(cols)}')
    print(f'DEBUG 0027: Has cmd={("cmd" in cols)}, pwd={("pwd" in cols)}, cmd_version={("cmd_version" in cols)}, process_id={("process_id" in cols)}')

    if 'cmd' not in cols or 'pwd' not in cols or 'cmd_version' not in cols:
        print('✓ Fresh install or fields already removed - skipping data copy')
        return

    # Check if process_id field exists (should exist from 0026)
    if 'process_id' not in cols:
        print('✗ ERROR: process_id field not found. Migration 0026 must run first.')
        return

    # Get or create Machine.current()
    machine_id = get_or_create_current_machine(cursor)

    # Get ArchiveResults without process_id that have cmd data
    # Use plugin (extractor was renamed to plugin in migration 0025)
    cursor.execute("""
        SELECT id, snapshot_id, plugin, cmd, pwd, cmd_version,
               status, start_ts, end_ts, created_at
        FROM core_archiveresult
        WHERE process_id IS NULL
        AND (cmd IS NOT NULL OR pwd IS NOT NULL)
    """)

    results = cursor.fetchall()

    if not results:
        print('✓ No ArchiveResults need Process migration')
        return

    print(f'Migrating {len(results)} ArchiveResults to Process records...')

    migrated_count = 0
    skipped_count = 0
    error_count = 0

    for i, row in enumerate(results):
        ar_id, snapshot_id, plugin, cmd_raw, pwd, cmd_version, status, start_ts, end_ts, created_at = row

        if i == 0:
            print(f'DEBUG 0027: First row: ar_id={ar_id}, plugin={plugin}, cmd={cmd_raw[:50] if cmd_raw else None}, status={status}')

        try:
            # Parse cmd field
            cmd_array = parse_cmd_field(cmd_raw)

            if i == 0:
                print(f'DEBUG 0027: Parsed cmd: {cmd_array}')

            # Extract binary info from cmd[0] if available
            binary_id = None
            if cmd_array and cmd_array[0]:
                binary_name = Path(cmd_array[0]).name or plugin  # Fallback to plugin name
                binary_abspath = cmd_array[0]
                binary_version = cmd_version or ''

                # Get or create Binary record
                binary_id = get_or_create_binary(
                    cursor, machine_id, binary_name, binary_abspath, binary_version
                )

                if i == 0:
                    print(f'DEBUG 0027: Created Binary: id={binary_id}, name={binary_name}')

            # Map status
            process_status, exit_code = map_status(status)

            # Set timestamps
            started_at = start_ts or created_at
            ended_at = end_ts if process_status == 'exited' else None

            # Create Process record
            process_id = create_process(
                cursor=cursor,
                machine_id=machine_id,
                pwd=pwd or '',
                cmd=cmd_array,
                status=process_status,
                exit_code=exit_code,
                started_at=started_at,
                ended_at=ended_at,
                binary_id=binary_id,
            )

            if i == 0:
                print(f'DEBUG 0027: Created Process: id={process_id}')

            # Link ArchiveResult to Process
            cursor.execute(
                "UPDATE core_archiveresult SET process_id = ? WHERE id = ?",
                [process_id, ar_id]
            )

            migrated_count += 1

            if i == 0:
                print(f'DEBUG 0027: Linked ArchiveResult to Process')

        except Exception as e:
            print(f'✗ Error migrating ArchiveResult {ar_id}: {e}')
            import traceback
            traceback.print_exc()
            error_count += 1
            continue

    print(f'✓ Migration complete: {migrated_count} migrated, {skipped_count} skipped, {error_count} errors')


class Migration(migrations.Migration):

    dependencies = [
        ('core', '0026_add_process_to_archiveresult'),
        ('machine', '0007_add_process_type_and_parent'),
    ]

    operations = [
        # First, copy data from old fields to Process
        migrations.RunPython(
            copy_archiveresult_data_to_process,
            reverse_code=migrations.RunPython.noop,
        ),

        # Now safe to remove old fields (moved from 0025)
        migrations.RemoveField(
            model_name='archiveresult',
            name='cmd',
        ),
        migrations.RemoveField(
            model_name='archiveresult',
            name='cmd_version',
        ),
        migrations.RemoveField(
            model_name='archiveresult',
            name='pwd',
        ),
    ]
