from __future__ import annotations

from typing import Any
from typing import cast
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple
from typing import Union
from uuid import UUID

import httpx
from kleinkram.api.client import AuthenticatedClient
from kleinkram.config import Config
from kleinkram.errors import MissionDoesNotExist
from kleinkram.errors import MissionExists
from kleinkram.errors import NoPermission
from kleinkram.models import DataType
from kleinkram.models import File
from kleinkram.models import FilesById
from kleinkram.models import FilesByMission
from kleinkram.models import FileState
from kleinkram.models import Mission
from kleinkram.models import MissionById
from kleinkram.models import MissionByName
from kleinkram.models import Project
from kleinkram.models import TagType
from kleinkram.utils import is_valid_uuid4


# TODO: change to 10000
MAX_PAGINATION = 1_000

TEMP_CREDS = "/file/temporaryAccess"
CLAIM_ADMIN = "/user/claimAdmin"

PROJECT_BY_NAME = "/project/byName"
PROJECT_BY_ID = "/project/one"
PROJECT_CREATE = "/project/create"
PROJECT_ALL = "/project/filtered"

MISSION_BY_NAME = "/mission/byName"
MISSION_BY_ID = "/mission/one"
MISSION_CREATE = "/mission/create"
MISSION_BY_PROJECT_NAME = "/mission/filteredByProjectName"
MISSION_UPDATE_METADATA = "/mission/tags"

ALL_USERS = "/user/all"
USER_INFO = "/user/me"
PROMOTE_USER = "/user/promote"
DEMOTE_USER = "/user/demote"

FILE_DOWNLOAD = "/file/download"
FILE_QUERY = "/file/filteredByNames"
FILE_ONE = "/file/one"
FILE_OF_MISSION = "/file/ofMission"

TAG_TYPE_BY_NAME = "/tag/filtered"

GET_STATUS = "/user/me"


def claim_admin(client: AuthenticatedClient) -> None:
    """\
    the first user on the system could call this
    """
    response = client.post(CLAIM_ADMIN)
    response.raise_for_status()
    return


def get_project(
    client: AuthenticatedClient, identifier: Union[str, UUID]
) -> Union[tuple[UUID, Dict[str, Any]], tuple[None, None]]:

    if isinstance(identifier, UUID):
        params = {"uuid": str(identifier)}
    else:
        params = {"name": identifier}

    resp = client.get("/missions", params=params)

    if resp.status_code in (403, 404):
        return None, None

    # TODO: handle other status codes
    resp.raise_for_status()

    details = resp.json()
    return UUID(details["uuid"], version=4), details


def create_project(
    client: AuthenticatedClient,
    project_name: str,
    *,
    description: str | None = None,
    check_exists: bool = False,
) -> UUID:
    """\
    creates a new mission with the given name and project_id

    if check_exists is True, the function will return the existing mission_id,
    otherwise if the mission already exists an error will be raised
    """
    if description is None:
        description = "autogenerated by CLI"

    if check_exists:
        project_id, _ = get_project(client, project_name)
        if project_id is not None:
            return project_id

    if is_valid_uuid4(project_name):
        raise ValueError(
            f"Project name: `{project_name}` is a valid UUIDv4, "
            "project names must not be valid UUIDv4's"
        )

    resp = client.post(
        MISSION_CREATE,
        json={
            "name": project_name,
            "description": description,
            "requiredTags": [],
        },
    )

    if resp.status_code >= 400:
        raise ValueError(
            f"Failed to create project. Status Code: "
            f"{str(resp.status_code)}\n"
            f"{resp.json()['message'][0]}"
        )

    return UUID(resp.json()["uuid"], version=4)


def get_mission_id_by_name(
    client: AuthenticatedClient, mission_name, project_id: UUID
) -> Optional[UUID]:
    params = {"name": mission_name, "projectUUID": str(project_id)}
    resp = client.get(MISSION_BY_NAME, params=params)

    if resp.status_code in (403, 404):
        return None

    # TODO: handle other status codes
    resp.raise_for_status()

    data = resp.json()

    return UUID(data["uuid"], version=4)


def get_mission_by_id(
    client: AuthenticatedClient, mission_id: UUID
) -> Optional[Mission]:
    params = {"uuid": str(mission_id), "take": MAX_PAGINATION}
    resp = client.get(FILE_OF_MISSION, params=params)

    if resp.status_code in (403, 404):
        return None

    resp.raise_for_status()
    data = resp.json()[0]
    files = [_parse_file(file) for file in data]

    resp = client.get(MISSION_BY_ID, params={"uuid": str(mission_id)})
    resp.raise_for_status()

    mission_data = resp.json()
    mission = Mission(
        id=mission_id,
        name=mission_data["name"],
        project_id=UUID(mission_data["project"]["uuid"], version=4),
        project_name=mission_data["project"]["name"],
        files=files,
    )

    return mission


def get_project_id_by_name(
    client: AuthenticatedClient, project_name: str
) -> Optional[UUID]:
    params = {"name": project_name}
    resp = client.get(PROJECT_BY_NAME, params=params)

    if resp.status_code in (403, 404):
        return None

    resp.raise_for_status()

    return UUID(resp.json()["uuid"], version=4)


def create_mission(
    client: AuthenticatedClient,
    project_id: UUID,
    mission_name: str,
    *,
    tags: Optional[Dict[UUID, str]] = None,
    ignore_missing_tags: bool = False,
) -> UUID:
    """\
    creates a new mission with the given name and project_id

    if check_exists is True, the function will return the existing mission_id,
    otherwise if the mission already exists an error will be raised
    """

    if get_mission_id_by_name(client, mission_name, project_id) is not None:
        raise MissionExists(f"Mission with name: `{mission_name}` already exists")

    if is_valid_uuid4(mission_name):
        raise ValueError(
            f"Mission name: `{mission_name}` is a valid UUIDv4, "
            "mission names must not be valid UUIDv4's"
        )

    payload = {
        "name": mission_name,
        "projectUUID": str(project_id),
        "tags": {str(k): v for k, v in tags.items()} if tags else {},
        "ignoreTags": ignore_missing_tags,
    }

    resp = client.post(MISSION_CREATE, json=payload)
    resp.raise_for_status()

    return UUID(resp.json()["uuid"], version=4)


def get_project_permission_level(client: AuthenticatedClient, project_id: UUID) -> int:
    """\
    we need this to check if a user has the permissions to
    create a mission in an existing project
    """

    resp = client.get("/user/permissions")
    resp.raise_for_status()

    project_group: List[Dict[str, Union[str, int]]] = resp.json().get("projects", [])
    filtered_by_id = filter(lambda x: x.get("uuid") == str(project_id), project_group)

    # it is possilbe that a user has access to a project via multiple groups
    # in this case we take the highest permission level
    return cast(int, max(map(lambda x: x.get("access", 0), filtered_by_id)))


def _parse_file(file: Dict[str, Any]) -> File:
    project_id = UUID(file["mission"]["project"]["uuid"], version=4)
    project_name = file["mission"]["project"]["name"]

    mission_id = UUID(file["mission"]["uuid"], version=4)
    mission_name = file["mission"]["name"]

    filename = file["filename"]
    file_id = UUID(file["uuid"], version=4)
    file_size = file["size"]
    file_hash = file["hash"]

    parsed = File(
        id=file_id,
        name=filename,
        size=file_size,
        hash=file_hash,
        project_id=project_id,
        project_name=project_name,
        mission_id=mission_id,
        mission_name=mission_name,
        state=FileState(file["state"]),
    )
    return parsed


def get_file(client: AuthenticatedClient, id: UUID) -> File:
    resp = client.get(FILE_ONE, params={"uuid": str(id)})
    resp.raise_for_status()

    return _parse_file(resp.json())


def get_files(
    client: AuthenticatedClient,
    name: Optional[str] = None,
    project: Optional[str] = None,
    mission: Optional[str] = None,
    topics: Optional[List[str]] = None,
    tags: Optional[Dict[str, str]] = None,
) -> List[File]:
    # TODO: allow to search by id

    params: Dict[str, Any] = {"take": MAX_PAGINATION}
    if name is not None:
        params["name"] = name
    if project is not None:
        params["projectName"] = project
    if mission is not None:
        params["missionName"] = mission
    if topics:
        params["topics"] = ",".join(topics)
    if tags:
        params["tags"] = tags

    resp = client.get(FILE_QUERY, params=params)
    resp.raise_for_status()

    files = []
    data = resp.json()

    for file in data:
        try:
            parsed = _parse_file(file)
            files.append(parsed)
        except Exception:
            print(f"Error parsing file: {file}")
    return files


def get_missions(
    client: AuthenticatedClient,
    project: Optional[str] = None,
    tags: Optional[Dict[str, str]] = None,
) -> list[Mission]:
    # TODO: use a better endpoint once this exists
    matching_files = get_files(client, project=project, tags=tags)

    ret = {}
    for file in matching_files:
        ret[file.mission_id] = Mission(
            id=file.mission_id,
            name=file.mission_name,
            project_id=file.project_id,
            project_name=file.project_name,
        )

    return list(ret.values())


def get_projects(client: AuthenticatedClient) -> list[Project]:
    resp = client.get(PROJECT_ALL)
    resp.raise_for_status()

    ret = []
    for pr in resp.json()[0]:
        id = UUID(pr["uuid"], version=4)
        name = pr["name"]
        description = pr["description"]
        ret.append(Project(id=id, name=name, description=description))

    return ret


def get_mission_by_spec(
    client: AuthenticatedClient, spec: Union[MissionById, MissionByName]
) -> Optional[Mission]:
    if isinstance(spec, MissionById):
        return get_mission_by_id(client, spec.id)

    if isinstance(spec.project, UUID):
        project_id = spec.project
    else:
        project_id = get_project_id_by_name(client, spec.project)
    if project_id is None:
        return None

    mission_id = get_mission_id_by_name(client, spec.name, project_id)
    if mission_id is None:
        return None

    return get_mission_by_id(client, mission_id)


def get_files_by_file_spec(
    client: AuthenticatedClient, spec: Union[FilesByMission, FilesById]
) -> List[File]:
    if isinstance(spec, FilesById):
        return [get_file(client, file_id) for file_id in spec.ids]

    parsed_mission = get_mission_by_spec(client, spec.mission)
    if parsed_mission is None:
        raise ValueError("mission not found")

    if spec.files:
        filtered = [
            f
            for f in parsed_mission.files
            if f.id in spec.files or f.name in spec.files
        ]
        return filtered

    return parsed_mission.files


def get_tag_type_by_name(
    client: AuthenticatedClient, tag_name: str
) -> Optional[TagType]:
    resp = client.get(TAG_TYPE_BY_NAME, params={"name": tag_name, "take": 1})

    if resp.status_code in (403, 404):
        return None

    resp.raise_for_status()

    data = resp.json()[0]
    tag_type = TagType(
        name=data["name"],
        id=UUID(data["uuid"], version=4),
        data_type=DataType(data["datatype"]),
        description=data["description"],
    )
    return tag_type


def get_tags_map(
    client: AuthenticatedClient, metadata: Dict[str, str]
) -> Dict[UUID, str]:
    # TODO: this needs a better endpoint
    ret = {}
    for key, val in metadata.items():
        tag_type = get_tag_type_by_name(client, key)

        if tag_type is None:
            print(f"tag: {key} not found")
            continue

        ret[tag_type.id] = val

    return ret


def update_mission_metadata(
    client: AuthenticatedClient, mission_id: UUID, metadata: Dict[str, str]
) -> None:
    tags_dct = get_tags_map(client, metadata)
    payload = {
        "missionUUID": str(mission_id),
        "tags": {str(k): v for k, v in tags_dct.items()},
    }
    resp = client.post(MISSION_UPDATE_METADATA, json=payload)

    if resp.status_code == 404:
        raise MissionDoesNotExist

    if resp.status_code == 403:
        raise NoPermission

    resp.raise_for_status()


def get_api_version() -> Tuple[int, int, int]:
    config = Config()
    client = httpx.Client()

    resp = client.get(f"{config.endpoint}{GET_STATUS}")
    vers = resp.headers["kleinkram-version"].split(".")

    return tuple(map(int, vers))  # type: ignore
