import hashlib
import io
import os
import traceback
import uuid
import zipfile
from datetime import datetime, timezone
from urllib.parse import urlparse
from xml.etree.ElementTree import Element, SubElement, tostring

import bleach
import sqlalchemy
from flask import Blueprint
from flask import current_app as app
from flask import jsonify, request, send_from_directory
from flask_babel import gettext
from flask_login import current_user
from werkzeug.datastructures.file_storage import FileStorage
from werkzeug.utils import secure_filename

from opentakserver.extensions import db, logger
from opentakserver.functions import format_bytes, iso8601_string_from_datetime
from opentakserver.models.DataPackage import DataPackage
from opentakserver.models.MissionContent import MissionContent

data_package_marti_api = Blueprint("data_package_marti_api", __name__)


def save_data_package_file(file, filename: str = None, username: str = None, eud_uid: str = None):
    file_hash = request.args.get("hash")
    if not file_hash:
        sha256 = hashlib.sha256()
        if type(file) == bytes:
            sha256.update(file)
            file_hash = sha256.hexdigest()
            filename, extension = os.path.splitext(secure_filename(filename))
            with open(os.path.join(app.config.get("UPLOAD_FOLDER"), f"{file_hash}.zip"), "wb") as f:
                f.write(file)
        else:
            sha256.update(file.stream.read())
            file.stream.seek(0)
            file_hash = sha256.hexdigest()
            file.save(os.path.join(app.config.get("UPLOAD_FOLDER"), f"{file_hash}.zip"))
            filename, extension = os.path.splitext(secure_filename(file.filename))
            logger.debug("Got file: {} - {}".format(file.filename, file_hash))

    file_size = os.path.getsize(os.path.join(app.config.get("UPLOAD_FOLDER"), f"{file_hash}.zip"))
    save_data_package_to_db(
        f"{filename}.zip", file_hash, "application/x-zip-compressed", file_size, username, eud_uid
    )

    return file_hash


def save_data_package_to_db(
    filename: str = None,
    sha256_hash: str = None,
    mimetype: str = "application/zip",
    file_size: int = 0,
    username: str = None,
    eud_uid: str = None,
):
    try:
        data_package = DataPackage()
        data_package.filename = filename
        data_package.hash = sha256_hash
        data_package.creator_uid = request.args.get("CreatorUid")  # iTAK
        data_package.creator_uid = request.args.get("creatorUid")  # All other TAK clients
        data_package.submission_user = current_user.id if current_user.is_authenticated else None
        data_package.submission_time = datetime.now(timezone.utc)
        data_package.mime_type = mimetype
        data_package.size = file_size
        data_package.creator_uid = eud_uid

        if username:
            user = app.security.datastore.find_user(username=username)
            if user:
                data_package.submission_user = user.id

        db.session.add(data_package)
        db.session.commit()
    except sqlalchemy.exc.IntegrityError as e:
        db.session.rollback()
        logger.error("Failed to save data package: {}".format(e))
        logger.debug(traceback.format_exc())
        return (
            jsonify(
                {"success": False, "error": gettext("This data package has already been uploaded")}
            ),
            400,
        )


def create_data_package_zip(file: FileStorage | str) -> str:
    if isinstance(file, str):
        logger.info(secure_filename(os.path.basename(file)))
        filename, extension = os.path.splitext(secure_filename(os.path.basename(file)))
        logger.info(f"Got filename {filename}")
    else:
        filename, extension = os.path.splitext(secure_filename(file.filename))

    zipf = zipfile.ZipFile(
        os.path.join(app.config.get("UPLOAD_FOLDER"), f"{filename}.zip"),
        "a",
        zipfile.ZIP_DEFLATED,
        False,
    )

    # Use the md5 of the uploaded file as its folder name in the data package zip
    md5 = hashlib.md5()
    try:
        md5.update(file.stream.read())
        file.stream.seek(0)
    except AttributeError:
        with open(file, "r") as f:
            md5.update(f.read().encode())

    md5_hash = md5.hexdigest()

    if isinstance(file, str):
        with open(file, "r") as f:
            zipf.writestr(f"{md5_hash}/{secure_filename(filename + extension)}", f.read())
    else:
        zipf.writestr(f"{md5_hash}/{secure_filename(filename + extension)}", file.stream.read())

    # MANIFEST file
    manifest = Element("MissionPackageManifest", {"version": "2"})
    config = SubElement(manifest, "Configuration")
    SubElement(config, "Parameter", {"name": "uid", "value": str(uuid.uuid4())})
    SubElement(
        config, "Parameter", {"name": "name", "value": secure_filename(filename + extension)}
    )

    contents = SubElement(manifest, "Contents")
    content = SubElement(
        contents,
        "Content",
        {"ignore": "false", "zipEntry": f"{md5_hash}/{secure_filename(filename + extension)}"},
    )

    extension = extension.lower().replace(".", "")
    if extension == "kml" or extension == "kmz":
        SubElement(
            content,
            "Parameter",
            {"name": "name", "value": f"{secure_filename(filename + extension)}"},
        )
        SubElement(content, "Parameter", {"name": "contentType", "value": "KML"})
        SubElement(content, "Parameter", {"name": "visible", "value": "true"})

    zipf.writestr("MANIFEST/manifest.xml", tostring(manifest))
    zipf.close()

    # Get the sha256 hash of the data package zip for its file name on disk and for the data_packages table
    zip_file = open(os.path.join(app.config.get("UPLOAD_FOLDER"), f"{filename}.zip"), "rb")
    zip_file_bytes = zip_file.read()
    zip_file.close()

    sha256 = hashlib.sha256()
    sha256.update(zip_file_bytes)
    data_package_hash = sha256.hexdigest()

    os.rename(
        os.path.join(app.config.get("UPLOAD_FOLDER"), f"{filename}.zip"),
        os.path.join(app.config.get("UPLOAD_FOLDER"), f"{data_package_hash}.zip"),
    )
    save_data_package_to_db(
        f"{filename}.zip", data_package_hash, "application/zip", len(zip_file_bytes)
    )

    return data_package_hash


@data_package_marti_api.route("/Marti/sync/missionupload", methods=["POST"])
def data_package_share():
    if not len(request.files):
        return jsonify({"success": False, "error": gettext("A file is required")}), 400
    for file in request.files:
        file = request.files[file]

        name, extension = os.path.splitext(file.filename)
        extension = extension.replace(".", "")

        # ATAK sends data packages as zips with a file name but no extension
        if not extension and "zip" in file.mimetype:
            extension = "zip"

        if extension.lower() not in app.config.get("ALLOWED_EXTENSIONS"):
            logger.info(
                f"file is {file.filename}, extension is {extension}, content-type {file.mimetype}"
            )
            return (
                jsonify(
                    {
                        "success": False,
                        "error": gettext(
                            "Invalid file extension: %(extension)s", extension=extension
                        ),
                    }
                ),
                400,
            )

        if extension != "zip":
            file_hash = create_data_package_zip(file)
        else:
            file_hash = save_data_package_file(file)

        url = urlparse(request.url_root)
        return (
            "https://{}:{}/Marti/api/sync/metadata/{}/tool".format(
                url.hostname, app.config.get("OTS_MARTI_HTTPS_PORT"), file_hash
            ),
            200,
        )


@data_package_marti_api.route("/Marti/api/sync/metadata/<file_hash>/tool", methods=["GET", "PUT"])
def data_package_metadata(file_hash):
    if request.method == "PUT":
        try:
            data_package = db.session.execute(
                db.select(DataPackage).filter_by(hash=file_hash)
            ).first()
            if data_package:
                data_package = data_package[0]
                data_package.keywords = bleach.clean(request.data.decode("utf-8"))
                db.session.add(data_package)
                db.session.commit()
                return "", 200
            else:
                return "", 404
        except BaseException as e:
            logger.error("Data package PUT failed: {}".format(e))
            logger.error(traceback.format_exc())
            return {"error": str(e)}, 500
    elif request.method == "GET":
        data_package = db.session.execute(
            db.select(DataPackage).filter_by(hash=file_hash)
        ).scalar_one()
        return send_from_directory(
            app.config.get("UPLOAD_FOLDER"),
            data_package.hash + ".zip",
            download_name=data_package.filename,
        )


@data_package_marti_api.route("/Marti/api/sync/search", methods=["GET"])
def get_data_package():
    data_package_hash = request.args.get("hash")
    data_package_uid = request.args.get("uid")

    # TODO: Support keywords
    keyword = request.args.get("keyword")

    query = db.select(DataPackage)
    if data_package_hash:
        query = query.where(DataPackage.hash == data_package_hash)

    # DataPackage uses the hash as the UID for some reason
    if data_package_uid:
        query = query.where(DataPackage.hash == data_package_uid)

    data_packages = db.session.execute(query).scalars()
    return_value = {
        "version": "3",
        "type": "gov.tak.api.comms.takserver.mission.data.Resource",
        "data": [],
        "messages": [],
        "nodeId": app.config.get("OTS_NODE_ID"),
    }

    for dp in data_packages:
        submission_user = "anonymous"
        if dp.user:
            submission_user = dp.user.username
        keywords = []
        if dp.keywords:
            keywords = dp.keywords.split(",")

        try:
            expiration = None
            if dp.expiration:
                expiration = int(dp.expiration)
            return_value["data"].append(
                {
                    "filename": dp.filename,
                    "keywords": keywords,
                    "mimeType": dp.mime_type,
                    "name": dp.filename,
                    "submissionTime": iso8601_string_from_datetime(dp.submission_time),
                    "submitter": submission_user,
                    "uid": dp.hash,
                    "creatorUid": dp.creator_uid,
                    "hash": dp.hash,
                    "size": dp.size,
                    "tool": dp.tool,
                    "groups": [],
                    "expiration": expiration,
                    "latitude": 0,
                    "longitude": 0,
                    "altitude": 0,
                }
            )
        except BaseException as e:
            logger.error(f"Data package GET failed: {e}")
            logger.debug(traceback.format_exc())

    return jsonify(return_value)


@data_package_marti_api.route("/Marti/sync/search", methods=["GET"])
def data_package_search():
    query = db.session.query(DataPackage)

    data_package_uid = request.args.get("uid")
    if data_package_uid:
        query = query.where(DataPackage.hash == data_package_uid)

    data_packages = db.session.execute(query).scalars()
    res = {"resultCount": 0, "results": []}
    for dp in data_packages:
        submission_user = "anonymous"
        if dp.user:
            submission_user = dp.user.username
        res["results"].append(
            {
                "UID": dp.hash,
                "Name": dp.filename,
                "Hash": dp.hash,
                "CreatorUid": dp.creator_uid,
                "SubmissionDateTime": dp.submission_time.strftime("%Y-%m-%dT%H:%M:%S.000Z"),
                "EXPIRATION": "-1",
                "Keywords": ["missionpackage"],
                "MIMEType": dp.mime_type,
                "Size": "{}".format(dp.size),
                "SubmissionUser": submission_user,
                "PrimaryKey": "{}".format(dp.id),
                "Tool": dp.tool if dp.tool else "public",
            }
        )
        res["resultCount"] += 1

    return jsonify(res)


@data_package_marti_api.route("/Marti/sync/content", methods=["GET", "HEAD"])
def download_data_package():
    file_hash = request.args.get("hash")
    file = db.session.execute(db.select(DataPackage).filter_by(hash=file_hash)).first()
    if not file:
        file = db.session.execute(db.select(MissionContent).filter_by(hash=file_hash)).first()
    if not file and request.method == "HEAD":
        return "", 404
    elif not file:
        return (
            jsonify({"success": False, "error": f"No data package found with hash {file_hash}"}),
            404,
        )
    elif file and request.method == "HEAD":
        return "", 200

    filename, extension = os.path.splitext(secure_filename(file[0].filename))
    if os.path.exists(os.path.join(app.config.get("UPLOAD_FOLDER"), f"{file_hash}{extension}")):
        return send_from_directory(
            app.config.get("UPLOAD_FOLDER"),
            f"{file_hash}{extension}",
            download_name=file[0].filename,
        )
    elif os.path.exists(
        os.path.join(app.config.get("OTS_DATA_FOLDER"), "missions", file[0].filename)
    ):
        return send_from_directory(
            os.path.join(app.config.get("OTS_DATA_FOLDER"), "missions"), file[0].filename
        )
    else:
        return (
            jsonify(
                {
                    "success": False,
                    "error": gettext("File not found: %(filename)s}", filename=file[0].filename),
                }
            ),
            404,
        )


@data_package_marti_api.route("/Marti/sync/missionquery")
def data_package_query():
    try:
        data_package = db.session.execute(
            db.select(DataPackage).filter_by(hash=request.args.get("hash"))
        ).scalar_one()
        if data_package:

            url = urlparse(request.url_root)
            return (
                "https://{}:{}/Marti/api/sync/metadata/{}/tool".format(
                    url.hostname, app.config.get("OTS_MARTI_HTTPS_PORT"), request.args.get("hash")
                ),
                200,
            )
        else:
            return jsonify({"success": False, "error": "File not found"}), 404
    except sqlalchemy.exc.NoResultFound as e:
        return jsonify({"success": False, "error": "File not found"}), 404


@data_package_marti_api.route("/Marti/api/files/metadata")
def get_metadata():
    mission_package = request.args.get("missionPackage")
    page = request.args.get("page")
    limit = request.args.get("limit")
    mission = request.args.get("mission")
    name = request.args.get("name")
    sort = request.args.get("sort")
    setting = request.args.get("setting")

    query = db.session.query(DataPackage)
    if name:
        query = query.filter_by(filename=bleach.clean(name))

    data_packages = db.session.execute(query).scalars()

    response = {
        "version": "3",
        "type": "gov.tak.api.comms.takserver.mission.data.Resource",
        "data": [],
        "nodeId": app.config.get("OTS_NODE_ID"),
    }
    for dp in data_packages:
        response["data"].append(
            {
                "Name": dp.filename,
                "User": dp.user.username if dp.user else None,
                "Creator": dp.eud.callsign if dp.eud else None,
                "Size": format_bytes(dp.size),
                "Time": iso8601_string_from_datetime(dp.submission_time),
                "MimeType": dp.mime_type,
                "Keywords": dp.keywords,
                "Expiration": dp.expiration,
                "Hash": dp.hash,
            }
        )

    return jsonify(response)
