import base64
import logging
import re
from dataclasses import dataclass
from datetime import datetime
from typing import Optional, TypedDict

import requests

from src.config import Configuration
from src.internals.cache.redis import get_conn
from src.internals.database.database import query_rowcount_db, query_one_db
from src.internals.database.database import cached_query
from src.utils.utils import clear_web_cache_for_archive

ARCHIVE_MIMES: list[str] = [
    "application/zip",
    "application/7z",
    "application/rar",
    "application/x-7z-compressed",
    "application/x-rar-compressed",
    "application/x-zip-compressed",
    "application/x-rar",
]


@dataclass
class File:
    id: int
    hash: str
    mtime: datetime
    ctime: datetime
    mime: str
    ext: str
    added: datetime
    size: int
    ihash: Optional[str]


class TDPost(TypedDict):
    file_id: int
    id: str
    user: str
    service: str
    title: str
    content: str
    published: str
    file: dict
    attachments: list


class TDDiscordPost(TypedDict):
    file_id: int
    id: str
    server: str
    channel: str
    content: str
    published: str
    embeds: list
    mentions: list
    attachments: list


class TDFileRelationships(TypedDict):
    posts: list[TDPost]
    discord_posts: list[TDDiscordPost]


def get_file_relationships(file_hash: str, reload: bool = False) -> TDFileRelationships:
    key = f"files:by_hash:{file_hash}"
    query = """
        SELECT
            files.*,
            (
                SELECT JSON_AGG(posts.*)
                FROM (
                    SELECT
                        file_id,
                        posts.id,
                        posts."user",
                        posts.service,
                        posts.title,
                        SUBSTRING(posts."content", 1, 50),
                        posts.published,
                        posts.file,
                        posts.attachments
                    FROM
                        file_post_relationships AS post_files
                        LEFT JOIN
                        posts
                        ON
                            post_files.post = posts.id
                            AND
                            post_files.service = posts.service
                    WHERE
                        files.id = post_files.file_id
                        AND
                        posts.id is not NULL
                    LIMIT 1000
                ) AS posts
            ) AS posts,
            (
                SELECT JSON_AGG(discord_posts.*)
                FROM (
                    SELECT
                        file_id,
                        discord_posts.id,
                        discord_posts."server",
                        discord_posts.channel,
                        SUBSTRING(discord_posts."content", 1, 50),
                        discord_posts.published,
                        discord_posts.embeds,
                        discord_posts.mentions,
                        discord_posts.attachments
                    FROM
                        file_discord_message_relationships AS discord_files
                        LEFT JOIN
                        discord_posts
                        ON
                            discord_files.id = discord_posts.id
                    WHERE
                        files.id = discord_files.file_id
                    LIMIT 1000
                ) AS discord_posts
            ) AS discord_posts
        FROM files
        WHERE files.hash = %s
        GROUP BY files.id;
    """
    return cached_query(query, key, (file_hash,), reload=reload, single=True)


def get_file(file_hash: str) -> Optional[File]:
    key = f"file:{file_hash}"
    query = "SELECT * FROM files WHERE hash = %s"
    if file := cached_query(query, key, (file_hash,), single=True):
        return File(**file)


def get_archive(file_hash: str) -> Optional[tuple[File, str]]:
    if not sha256_pattern.match(file_hash):
        return None
    if file := get_file(file_hash):
        return (file, file.ext) if file.mime in ARCHIVE_MIMES else None


@dataclass
class ArchiveInfo:
    file: File
    file_list: list[str]
    password: Optional[str]


archive_server_session = requests.Session()


def get_archive_files(file_hash: str) -> Optional[ArchiveInfo]:
    if not Configuration().archive_server["enabled"]:
        return None

    arc_data = get_archive(file_hash)

    if not arc_data:
        return None

    file, ext = arc_data
    key = f"archive_files:{file.hash}"
    query = """
        SELECT
            archive_files.file_id,
            archive_files.files,
            archive_files.password
        FROM
            archive_files
            LEFT JOIN
            files
            ON
                archive_files.file_id = files.id
        WHERE
            files.hash = %s
    """
    result = cached_query(query, key, (file.hash,), single=True)

    if result:
        return ArchiveInfo(
            file,
            result.get("files", []),
            result.get("password"),
        )
    else:
        try:
            files_api_call = archive_server_session.get(
                f"{Configuration().archive_server["api_url"]}/list/data/{file_hash[0:2]}/{file_hash[2:4]}/{file_hash}{ext}"
            )

            if files_api_call.status_code == 401:
                files: list[str] = []
            else:
                files_api_call.raise_for_status()
                files: list[str] = files_api_call.json()
            needs_pass_api_call = archive_server_session.get(
                f"{Configuration().archive_server["api_url"]}/needs_password/data/{file_hash[0:2]}/{file_hash[2:4]}/{file_hash}{ext}"
            )
            needs_pass_api_call.raise_for_status()

            assert needs_pass_api_call.text in ("true", "false")

            needs_pass = needs_pass_api_call.text == "true"

        except Exception as e:
            logging.exception("Failed to call archive server", exc_info=True)

            return None

        password = "" if needs_pass else None
        query_rowcount_db(
            """
            INSERT INTO archive_files
                (file_id, files, password)
            VALUES
                (%s, %s, %s)
            ON CONFLICT
                (file_id) DO NOTHING
            """,
            (file.id, files, password)
        )

        return ArchiveInfo(file, files, password)


def try_set_password(file_hash: str, passwords: list[str]) -> bool:
    arc_data = get_archive(file_hash)

    if not arc_data:
        return False

    (_, ext) = arc_data
    url = f"{Configuration().archive_server["api_url"]}/try_password/data/{file_hash[0:2]}/{file_hash[2:4]}/{file_hash}{ext}"
    password_call = requests.post(
        url,
        json={"passwords": passwords},
    )
    password: str = password_call.text

    if password:
        update_result = query_one_db(
            """
            UPDATE
                archive_files AS af
            SET
                password = %s
            FROM
                files
            WHERE
                files.hash = %s
                AND
                af.file_id = files.id
            RETURNING
                af.files
            """,
            (password, file_hash),
        )
        files: list[str] | None = update_result["files"] if update_result else None

        if not files:
            try:
                files_api_call = archive_server_session.get(
                    f"{Configuration().archive_server["api_url"]}/list/data/{file_hash[0:2]}/{file_hash[2:4]}/{file_hash}{ext}",
                    params=None if not password else {"password": password},
                )
                files_api_call.raise_for_status()
                files: list[str] = files_api_call.json()
                query_one_db(
                    """
                    UPDATE archive_files af
                    SET files = %s
                    FROM files as f
                    WHERE f.hash = %s AND af.file_id = f.id
                    """,
                    (files, file_hash),
                )

            except Exception:
                logging.exception("Failed to update empty list of archives")

        get_conn().delete(f"archive_files:{file_hash}")
        clear_web_cache_for_archive(file_hash)

        return True

    return False


sha256_pattern = re.compile(r'^[0-9a-fA-F]{64}$')
