import re
from pathlib import Path
from queue import Queue
from typing import Dict
from typing import Generator
from typing import List

import sqlite_utils

from bulk_restore_tool.models import Archive
from bulk_restore_tool.models import FileMetadata
from bulk_restore_tool.models import FileSelection
from bulk_restore_tool.models import RestorableFile
from bulk_restore_tool.models import FileStatus


LATEST_VERSION_VIEW_SQL = """
    WITH latest_versions AS (
        SELECT fileId, max(sourceLastModifiedMillis) as sourceLastModifiedMillis
        FROM file
        GROUP BY fileId
    )
    SELECT
        f.archiveId,
        f.fileId,
        f.backupSetId,
        f.fileType,
        f.path,
        f.versionTimeMillis,
        f.sourceLastModifiedMillis,
        f.sourceLengthBytes,
        f.sourceSHA256Checksum,
        f.sourceMD5Checksum,
        f.deleted,
        f.deletionTimeMillis,
        f.status,
        f.excludeReason
    FROM file f
    JOIN latest_versions v on (f.fileId = v.fileId)
    WHERE f.sourceLastModifiedMillis = v.sourceLastModifiedMillis
"""


RESTORABLE_VIEW_SQL = """
    SELECT
        a.deviceId,
        a.archiveId,
        a.destinationName,
        a.storageNodeWebUrl,
        f.fileId,
        f.path,
        f.backupSetId,
        f.versionTimemillis,
        f.sourceLastModifiedMillis,
        f.sourceLengthBytes,
        f.sourceSHA256Checksum,
        f.sourceMD5Checksum,
        f.status
    FROM latest_versions f
    JOIN archive a on (a.archiveId = f.archiveId)
    WHERE f.fileType != 'DIRECTORY'
    AND f.status = 'SELECTED'
"""


def select_files(
    archiveId: str, files: List[Dict], selection: FileSelection
) -> Generator[Dict, None, None]:
    """
    Marks files as 'selected' if they meet file selection criteria.
    If not selected, the 'excludeReason' field will be populated.
    """
    include_patterns = [re.compile(pat) for pat in selection.includePatterns]
    exclude_patterns = [re.compile(pat) for pat in selection.excludePatterns]
    for file in files:
        model = FileMetadata(**file, archiveId=archiveId)

        # apply include patterns
        if include_patterns:
            for regex in include_patterns:
                if regex.match(model.path):
                    model.status = FileStatus.SELECTED
                    model.excludeReason = None
        else:
            model.status = FileStatus.SELECTED
            model.excludeReason = None

        # skip other filter logic if file isn't included
        if not model.status == FileStatus.SELECTED:
            model.excludeReason = "Didn't match any include patterns."
            yield model.dict()

        # apply exclude patterns
        if exclude_patterns:
            for regex in exclude_patterns:
                if regex.match(model.path):
                    model.status = FileStatus.EXCLUDED
                    model.excludeReason = f"Matched exclude pattern: {regex.pattern}"

        # apply backup set filter
        if selection.backupSetIds:
            if model.backupSetId is None:
                raise V1BackupSetException(
                    f"{model.archiveId} is a V1 archive and backup sets are not accessible for restores. Unable to filter by backup set ID."
                )
            if model.backupSetId not in selection.backupSetIds:
                model.status = FileStatus.EXCLUDED
                model.excludeReason = (
                    f"Not in selected backupSetIds: {selection.backupSetIds}"
                )

        # apply deleted file age filter
        if selection.includeDeleted and selection.deletedAgeMillis is not None:
            if model.deleted and model.deletionTimeMillis < selection.deletedAgeMillis:
                model.status = FileStatus.EXCLUDED
                model.excludeReason = (
                    f"Delete prior to maxDeletedAge: {selection.deletedMaxAge}"
                )
        yield model.dict()


def save_archive_metadata_sqlite(
    archive: Archive,
    files: List[dict],
    target: Path,
    selection: FileSelection,
) -> Path:
    file_path = target / archive.db_filename
    db = sqlite_utils.Database(file_path)
    archive_table = db["archive"].create(  # type: ignore
        columns={name: field.type_ for name, field in Archive.__fields__.items()},
        pk="archiveId",
        if_not_exists=True,
    )
    file_table = db["file"].create(  # type: ignore
        columns={name: field.type_ for name, field in FileMetadata.__fields__.items()},
        pk=("archiveId", "fileId"),
        if_not_exists=True,
    )
    archive_table.upsert(archive.dict(), pk="archiveId")
    selected_files = select_files(
        archiveId=archive.archiveId, files=files, selection=selection
    )
    file_table.upsert_all(selected_files, pk=("archiveId", "fileId"))

    db.create_view("latest_versions", LATEST_VERSION_VIEW_SQL, replace=True)
    db.create_view("restorable", RESTORABLE_VIEW_SQL, replace=True)
    return file_path


def restore_queue(db: sqlite_utils.Database, archive_id: str) -> Queue:
    queue: Queue = Queue()
    for file in db.query(
        """
        SELECT *
        FROM restorable
        WHERE archiveId = :archiveId
        ORDER BY sourceLengthBytes ASC
        """,
        {"archiveId": archive_id},
    ):
        queue.put(RestorableFile(**file))
    return queue


def get_file_status(file: RestorableFile, db: sqlite_utils.Database) -> FileStatus:
    result = db.execute(
        sql="select status from file where fileId = :fileId and archiveId = :archiveId",
        parameters={"fileId": file.fileId, "archiveId": file.archiveId},
    ).fetchone()
    return FileStatus(result[0])


def set_file_status(
    file: RestorableFile, status: FileStatus, db: sqlite_utils.Database
):
    with db.conn:
        db.execute(
            sql="update file set status = :status where fileId = :fileId",
            parameters={"status": status, "fileId": file.fileId},
        )


class V1BackupSetException(Exception):
    ...
