from __future__ import annotations

from io import StringIO
from typing import Any

import django
from django.core.management import call_command
from django.db import connections
from django.db.migrations.executor import MigrationExecutor


def _get_executor(connection_alias: str = 'default') -> MigrationExecutor:
    return MigrationExecutor(connections[connection_alias])


def get_migration_status(connection_alias: str = 'default') -> dict[str, Any]:
    executor = _get_executor(connection_alias)
    plan = executor.migration_plan(executor.loader.graph.leaf_nodes())

    pending: list[dict[str, str]] = []
    for migration, backwards in plan:
        if backwards:
            continue
        pending.append({
            'app': migration.app_label,
            'name': migration.name,
            'label': f'{migration.app_label}.{migration.name}',
        })

    by_app: dict[str, list[str]] = {}
    for item in pending:
        by_app.setdefault(item['app'], []).append(item['name'])

    return {
        'django_version': django.get_version(),
        'pending_count': len(pending),
        'applied_count': len(executor.loader.applied_migrations),
        'up_to_date': len(pending) == 0,
        'pending': pending,
        'pending_by_app': [
            {'app': app, 'migrations': names}
            for app, names in sorted(by_app.items())
        ],
    }


def run_pending_migrations(connection_alias: str = 'default') -> dict[str, Any]:
    before = get_migration_status(connection_alias)
    if before['up_to_date']:
        return {
            'applied_count': 0,
            'output': 'Database is already up to date.',
            'status': before,
        }

    stdout = StringIO()
    stderr = StringIO()
    try:
        call_command(
            'migrate',
            verbosity=2,
            stdout=stdout,
            stderr=stderr,
            noinput=True,
            database=connection_alias,
        )
    except Exception as exc:
        output = stdout.getvalue().strip()
        err_output = stderr.getvalue().strip()
        detail = '\n'.join(part for part in (str(exc), output, err_output) if part)
        raise RuntimeError(detail) from exc

    output = stdout.getvalue().strip()
    err_output = stderr.getvalue().strip()
    if err_output:
        output = f'{output}\n{err_output}'.strip() if output else err_output

    after = get_migration_status(connection_alias)
    applied_count = before['pending_count'] - after['pending_count']

    return {
        'applied_count': max(applied_count, 0),
        'output': output or 'Migrations completed successfully.',
        'status': after,
    }
