import hashlib
import pathlib
import re
import sqlite3
import sys
from typing import Annotated

import requests
import typer

LINK_PATTERN = (
    r"(?:https://kdrive\.infomaniak\.com/app/collaborate/)?([0-9]+)/([0-9a-f-]+)"
)
TOKEN_URL = "https://kdrive.infomaniak.com/2/app/{kdrive_id}/dropbox/{box_uuid}/token"
USER_URL = "https://kdrive.infomaniak.com/2/app/{kdrive_id}/dropbox/{box_uuid}/user"
UPLOAD_URL = "https://kdrive.infomaniak.com/2/app/{kdrive_id}/dropbox/{box_uuid}/upload"
STATE_DB = ".kdu_state.sqlite3"


class KDUError(Exception):
    pass


def _raise_for_status(resp: requests.Response, *args, **kwargs):
    try:
        resp.raise_for_status()
    except requests.exceptions.HTTPError as e:
        e.add_note(f"content: {resp.text}")
        raise KDUError(str(e)) from e


class State:
    def __init__(self, state_db_path: pathlib.Path):
        self.state_db_path = state_db_path
        self.query(
            "CREATE TABLE IF NOT EXISTS state (path TEXT PRIMARY KEY, hash TEXT)"
        )

    def query(self, sql, params=None):
        with sqlite3.connect(self.state_db_path) as conn:
            return conn.execute(sql, [] if params is None else params).fetchone()


def main(
    link: str,
    path: Annotated[pathlib.Path, typer.Argument()] = pathlib.Path(),
    last_name="kdrive",
    first_name="uploader",
    email="kdrive-uploader@ik.me",
    dry_run: bool = False,
):
    # check file
    if not path.exists():
        raise KDUError(f"Path `{path.absolute()}` does not exist.")

    # parse the link
    match = re.match(LINK_PATTERN, link)
    if not match:
        raise KDUError(
            f"Could not match link `{link}`. It must either be a full kDrive drop box link `https://kdrive.infomaniak.com/app/collaborate/0000000/00000000-0000-0000-0000-000000000000` or just the ids segments `0000000/00000000-0000-0000-0000-000000000000`"
        )
    kdrive_id = match.group(1)
    box_uuid = match.group(2)

    # collect files
    if path.is_file():
        files = [path]
        root = path.parent
        for parent in reversed(path.parents):
            if parent.joinpath(STATE_DB).exists():
                root = parent
                break
    else:
        files = list(f for f in path.rglob("*"))
        root = path

    # prepare state
    state_db_path = root.joinpath(STATE_DB)
    state = State(state_db_path)

    # list all files
    files = [f for f in files if f != state_db_path]
    files = [f for f in files if f.is_file()]
    print(f"Found {len(files)} files")

    # prepare session
    session = requests.sessions.Session()
    session.hooks = {"response": _raise_for_status}

    # get the client token
    token_url = TOKEN_URL.format(kdrive_id=kdrive_id, box_uuid=box_uuid)
    token_resp = session.get(token_url)
    token_data = token_resp.json()
    token = token_data["data"]["uuid"]
    session.headers["Ik-Dropbox-Token"] = token

    # set the user
    user_url = USER_URL.format(kdrive_id=kdrive_id, box_uuid=box_uuid)
    user_payload = dict(
        first_name=first_name,
        last_name=last_name,
        email=email,
    )
    session.post(user_url, json=user_payload)

    # do the upload
    count_new, count_update, count_unchanged = 0, 0, 0
    for i, file in enumerate(files):
        disp = str(file)
        if len(disp) > 64:
            disp = disp[0:63] + "…"
        print(f"File {i + 1:>5}/{len(files)} {disp:<64}", end="\r")
        hash = hashlib.file_digest(file.open("rb"), "sha256").hexdigest()

        row = state.query("SELECT hash FROM state WHERE path = ?", (str(file),))
        old_hash = row[0] if row else None

        if old_hash is None:
            count_new += 1
        elif hash != old_hash:
            count_update += 1
        else:
            count_unchanged += 1
            continue

        upload_url = UPLOAD_URL.format(kdrive_id=kdrive_id, box_uuid=box_uuid)
        upload_params = dict(
            directory_path=str(file.parent.relative_to(root)),
            file_name=file.name,
            total_size=file.stat().st_size,
            last_modified_at=int(file.stat().st_mtime),
            total_chunk_hash=f"sha512:{hashlib.sha512(file.read_bytes()).hexdigest()}",
            chunk_number=1,
            chunk_size=file.stat().st_size,
            client_token=token,
        )
        if not dry_run:
            session.post(upload_url, params=upload_params, data=file.open("rb"))
            state.query("INSERT OR REPLACE INTO state VALUES (?, ?)", (str(file), hash))
    print()
    if dry_run:
        print("THIS WAS A DRY RUN, NOTHING WAS ACTUALLY UPLOADED")
    print(f"Created {count_new}, Updated {count_update}, Unchanged {count_unchanged}")


def run_cli():
    try:
        typer.run(main)
    except KDUError as e:
        print(e)
        sys.exit(1)


if __name__ == "__main__":
    run_cli()
