import re
from datetime import datetime, timezone
from pathlib import Path
from typing import List, Optional, Dict
from uuid import uuid4

import click
import sqlite_utils
from pydantic import BaseModel, root_validator, PrivateAttr, Extra, validator
from pydantic import Field
from rich import filesize


class Archive(BaseModel):
    sessionId: Optional[str]
    archiveId: str
    principalId: str
    username: str
    deviceId: str
    deviceName: str
    destinationId: str
    destinationName: str
    storageNodeWebUrl: str

    @property
    def json_filename(self):
        return (
            f"{self.username}_{self.deviceName}_{self.deviceId}_{self.archiveId}.json"
        )

    @property
    def db_filename(self):
        return f"{self.username}_{self.deviceName}_{self.deviceId}.db"


class FileMetadata(BaseModel, extra=Extra.ignore):
    archiveId: str
    fileId: str
    backupSetId: int
    fileType: str
    path: str
    versionTimeMillis: int
    sourceLastModifiedMillis: int
    sourceLengthBytes: int
    sourceSHA256Checksum: str
    deleted: bool
    deletionTimeMillis: Optional[int]
    selected: bool = False
    completed: bool = False
    excludeReason: Optional[str]

    @root_validator(pre=True)
    def handle_deleted_file(cls, values: dict):  # noqa
        if values["deleted"]:
            last_non_deleted = values.pop("lastNonDeletedEntry")
            values["deletionTimeMillis"] = values["versionTimeMillis"]
            values.update(last_non_deleted)
        return values


class RestorableFile(BaseModel):
    deviceId: str
    archiveId: str
    storageNodeWebUrl: str
    destinationName: str
    fileId: str
    path: str
    versionTimeMillis: int
    sourceLastModifiedMillis: int
    sourceLengthBytes: int
    sourceSHA256Checksum: str
    backupSetId: int
    error: Optional[str]
    failure_count: int = 0
    completed: bool = False

    @property
    def restore_path(self):
        # Convert Windows drive to letter folder
        value = re.sub(r"^([A-Z]):", r"\1", self.path)
        # remove root slash "/" if present
        value = value.lstrip("/")
        return f"{self.deviceId}/{value}"

    @property
    def size(self):
        """Human-readable file size. 1KB == 1000 Bytes."""
        return filesize.decimal(self.sourceLengthBytes)


class FileSelection(BaseModel):
    includeDeleted: bool = False
    backupSetIds: Optional[List[int]]
    deletedMaxAge: Optional[datetime]
    restoreAsOf: Optional[datetime]
    includePatterns: List[str] = Field(default_factory=list)
    excludePatterns: List[str] = Field(default_factory=list)

    @validator("includePatterns")
    def validate_include_regexes(cls, value):
        try:
            [re.compile(v) for v in value]
            return value
        except re.error as regex_error:
            raise click.ClickException(
                f"Unable to parse regex for include pattern '{regex_error.pattern}': {regex_error}"
            )

    @validator("excludePatterns")
    def validate_exclude_regexes(cls, value):
        try:
            [re.compile(v) for v in value]
            return value
        except re.error as regex_error:
            raise click.ClickException(
                f"Unable to parse regex for exclude pattern '{regex_error.pattern}': {regex_error}"
            )

    @property
    def endTimeMillis(self):
        if self.restoreAsOf is not None:
            return int(self.restoreAsOf.astimezone(timezone.utc).timestamp() * 1000)

    @property
    def deletedAgeMillis(self):
        if self.deletedMaxAge is not None:
            return int(self.deletedMaxAge.astimezone(timezone.utc).timestamp() * 1000)


class Device:
    def __init__(self, username: str, name: str, deviceId: str, db_file: Path):
        self.username = username
        self.name = name
        self.deviceId = deviceId
        self.db = sqlite_utils.Database(db_file)
        self.progress = None
        self.archive_progress = None
        self.task_id = None

    @property
    def completedFiles(self):
        result = next(
            self.db.query(
                """
                SELECT coalesce(count(fileId), 0) as completedFiles
                FROM file
                WHERE fileType != 'DIRECTORY'
                AND selected
                AND completed
                """
            )
        )
        return result["completedFiles"]

    @property
    def completedBytes(self):
        result = next(
            self.db.query(
                """
                SELECT coalesce(sum(sourceLengthBytes), 0) as completedBytes
                FROM file
                WHERE fileType != 'DIRECTORY'
                AND selected
                AND completed
                """
            )
        )
        return result["completedBytes"]

    @property
    def totalFiles(self):
        result = next(
            self.db.query(
                """
                SELECT coalesce(count(distinct(fileId)), 0) as totalFiles
                FROM file
                WHERE fileType != 'DIRECTORY' AND selected
                """
            )
        )
        return result["totalFiles"]

    @property
    def totalBytes(self):
        result = next(
            self.db.query(
                """
                with deduplicated as (
                    SELECT sourceLengthBytes, max(sourceLastModifiedMillis) as sourceLastModifiedMillis
                    FROM file
                    WHERE fileType != 'DIRECTORY' AND selected
                    GROUP BY path
                )
                SELECT coalesce(sum(sourceLengthBytes), 0) as totalBytes
                FROM deduplicated
                """
            )
        )
        return result["totalBytes"]


class RestoreJob(BaseModel):
    jobId: str = Field(default_factory=lambda: str(uuid4()))
    type: str
    identifier: str
    selection: FileSelection = Field(default_factory=FileSelection)
    targetFolder: Path = Field(default_factory=lambda: Path().absolute())
    archives: List[Archive]
    totalFileCount: int = 0
    totalByteCount: int = 0
    _devices: Dict[str, Device] = PrivateAttr(default_factory=dict)

    @property
    def devices(self):
        if not self._devices:
            for archive in self.archives:
                if archive.deviceId not in self._devices:
                    self._devices[archive.deviceId] = Device(
                        username=archive.username,
                        name=archive.deviceName,
                        deviceId=archive.deviceId,
                        db_file=self.folder / archive.db_filename,
                    )
        return self._devices

    @property
    def filename(self):
        return f"restore_{self.jobId}.json"

    @property
    def folder(self):
        return self.targetFolder / self.jobId

    def calculate_stats(self):
        self.totalFileCount = self.totalByteCount = 0
        for device in self.devices.values():
            self.totalFileCount += device.totalFiles
            self.totalByteCount += device.totalBytes
