"""Administrator backup UI, using PostgreSQL tools rather than legacy SQL Server commands."""
import datetime as dt
import json
import os
from pathlib import Path
import re
import subprocess
import uuid

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.engine import make_url
from sqlalchemy.orm import Session

from flyway.api.deps import get_db, require_admin_actor
from flyway.api.lists import Page, page_params
from flyway.config import settings
from flyway.domain.actor import Actor
from flyway.domain.errors import BusinessError
from flyway.services.audit import record

router = APIRouter(prefix='/api/backups', dependencies=[Depends(require_admin_actor)])
BACKUP_ID = re.compile(r'\d{8}T\d{12}Z-[a-f0-9]{8}\Z')


class BackupIn(BaseModel):
    note: str = ''


class DeleteIn(BaseModel):
    ids: list[str]


def directory() -> Path:
    if not settings.backup_dir:
        raise BusinessError('Database backups are not configured (FLYWAY_BACKUP_DIR).')
    return Path(settings.backup_dir).resolve()


def backup_path(id: str) -> Path:
    if not BACKUP_ID.fullmatch(id):
        raise HTTPException(status_code=404)
    return directory() / (id + '.dump')


def postgres_tool(tool: str, arguments: list[str]) -> None:
    url = make_url(settings.database_url)
    env = {**os.environ, 'PGHOST': url.host or 'localhost', 'PGPORT': str(url.port or 5432),
           'PGUSER': url.username or '', 'PGPASSWORD': url.password or '', 'PGDATABASE': url.database or '',
           'PGCONNECT_TIMEOUT': '10'}
    if 'sslmode' in url.query:
        env['PGSSLMODE'] = url.query['sslmode']
    try:
        subprocess.run([tool, *arguments], check=True, timeout=600, env=env,
                       stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
    except (OSError, subprocess.SubprocessError) as error:
        # Connection details and tool stderr may contain credentials or business data.
        raise BusinessError(f'{tool} failed. Check the server backup log and PostgreSQL client installation.') from error


@router.get('')
def list_backups(keyword: str = '', page: Page = Depends(page_params)):
    folder = directory()
    rows = []
    if folder.is_dir():
        for meta in sorted(folder.glob('*.json'), reverse=True):
            if not BACKUP_ID.fullmatch(meta.stem) or not meta.with_suffix('.dump').is_file():
                continue
            row = json.loads(meta.read_text(encoding='utf-8'))
            if keyword.casefold() in f"{row['note']} {row['operator']}".casefold():
                rows.append(row)
    start = (page.page - 1) * page.page_size
    return {'items': rows[start:start + page.page_size], 'total': len(rows)}


@router.post('')
def backup(body: BackupIn, db: Session = Depends(get_db), actor: Actor = Depends(require_admin_actor)):
    now = dt.datetime.now(dt.UTC)
    id = now.strftime('%Y%m%dT%H%M%S%fZ-') + uuid.uuid4().hex[:8]
    path = backup_path(id)
    path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
    pending = path.with_suffix('.partial')
    try:
        postgres_tool('pg_dump', ['--format=custom', '--no-owner', '--no-privileges', '--file', str(pending)])
        pending.replace(path)
        path.chmod(0o600)
        row = {'id': id, 'date': now.isoformat(), 'note': body.note, 'operator': actor.username}
        meta = path.with_suffix('.json')
        meta.write_text(json.dumps(row, ensure_ascii=False), encoding='utf-8')
        meta.chmod(0o600)
    finally:
        pending.unlink(missing_ok=True)
    record(db, actor, 'backup', 'database_backup', id, body.note)
    db.commit()
    return row


@router.post('/delete', status_code=204)
def delete(body: DeleteIn, db: Session = Depends(get_db), actor: Actor = Depends(require_admin_actor)):
    paths = [backup_path(id) for id in body.ids]
    for path in paths:
        path.unlink(missing_ok=True)
        path.with_suffix('.json').unlink(missing_ok=True)
        record(db, actor, 'delete', 'database_backup', path.stem)
    db.commit()


@router.post('/{id}/restore', status_code=204)
def restore(id: str, db: Session = Depends(get_db), actor: Actor = Depends(require_admin_actor)):
    path = backup_path(id)
    if not path.is_file():
        raise HTTPException(status_code=404)
    # Release the request's read transaction before the restore takes table locks.
    db.commit()
    postgres_tool('pg_restore', ['--clean', '--if-exists', '--single-transaction', '--exit-on-error',
                               '--no-owner', '--no-privileges', '--dbname', make_url(settings.database_url).database, str(path)])
    record(db, actor, 'restore', 'database_backup', id)
    db.commit()
