import json
import os
import pathlib

import jinja2
from flask import Flask, g, make_response, render_template, request, send_from_directory, session, jsonify
from flask.json.provider import JSONProvider

from src.config import Configuration
from src.lib.api import create_api_v2_client_error_response, TDAPIError

app = Flask(
    __name__,
    static_folder=Configuration().webserver["static_folder"],
    template_folder=Configuration().webserver["template_folder"],
)

if jinja_bytecode_cache_path := Configuration().webserver["jinja_bytecode_cache_path"]:
    pathlib.Path(jinja_bytecode_cache_path).mkdir(parents=True, exist_ok=True)
    app.jinja_env.bytecode_cache = jinja2.FileSystemBytecodeCache(jinja_bytecode_cache_path)

if Configuration().open_telemetry_endpoint:
    from src.internals.tracing.tracing import open_telemetry_init

    open_telemetry_init(app, Configuration().open_telemetry_endpoint)

import datetime
import logging
import re
from datetime import timedelta
from os import getenv, listdir
from os.path import exists, join, splitext
from typing import TypedDict
from urllib.parse import urljoin, quote_plus

import dateutil.parser
import humanize
import orjson

from src.lib.account import is_logged_in, load_account
from src.lib.notification import count_new_notifications
from src.lib.post import get_fileserver_for_value
from src.pages.api import api_bp
from src.types.account import Account
from src.utils.utils import (
    freesites,
    paysites,
    render_page_data,
    sanitize_html,
    url_is_for_non_logged_file_extension,
    parse_int,
)

app.url_map.strict_slashes = False

app.register_blueprint(api_bp)


app.config.update(
    dict(
        ENABLE_PASSWORD_VALIDATOR=True,
        ENABLE_LOGIN_RATE_LIMITING=True,
        SESSION_REFRESH_EACH_REQUEST=False,
        SESSION_COOKIE_SAMESITE="Lax",
        SECRET_KEY=Configuration().webserver["secret"],
        cache_TYPE="null" if Configuration().development_mode else "simple",
        CACHE_DEFAULT_TIMEOUT=None if Configuration().development_mode else 60,
        SEND_FILE_MAX_AGE_DEFAULT=0,
        TEMPLATES_AUTO_RELOAD=True if Configuration().development_mode else False,
    )
)


def file_url(file: TypedDict("FILE", {"hash": str, "ext": str})) -> str:
    file_hash = file["hash"]
    ext = file["ext"]
    fn = f"/data/{file_hash[0:2]}/{file_hash[2:4]}/{file_hash}.{ext}"
    fs = get_fileserver_for_value(fn)
    return fs + fn


class ORJSONProvider(JSONProvider):
    def __init__(self, *args, **kwargs):
        self.options = kwargs
        super().__init__(*args, **kwargs)

    def loads(self, s, **kwargs):
        if "object_hook" in kwargs:
            return json.loads(s, **kwargs)
        return orjson.loads(s)

    def dumps(self, obj, **kwargs):
        return orjson.dumps(obj, option=orjson.OPT_NON_STR_KEYS).decode("utf-8")


app.json = ORJSONProvider(app)


def simple_datetime(obj: str | datetime.datetime) -> str:
    if isinstance(obj, str):
        obj = dateutil.parser.parse(obj)
    return obj.strftime("%Y-%m-%d %H:%M:%S")


app.jinja_options = dict(trim_blocks=True, lstrip_blocks=True)
app.jinja_env.globals.update(is_logged_in=is_logged_in)
app.jinja_env.globals.update(render_page_data=render_page_data)
app.jinja_env.filters["relative_date"] = lambda val: humanize.naturaltime(val)
app.jinja_env.filters["simple_date"] = lambda dt: dt.strftime("%Y-%m-%d")  # maybe change
app.jinja_env.filters["simple_datetime"] = simple_datetime
app.jinja_env.filters["regex_match"] = lambda val, rgx: re.search(rgx, val)
app.jinja_env.filters["regex_find"] = lambda val, rgx: re.findall(rgx, val)
app.jinja_env.filters["sanitize_html"] = sanitize_html
app.jinja_env.filters["file_url"] = file_url  # todo use this instead of "{{ fancard.server or '' }}{{ fancard.path }}"
app.jinja_env.filters["quote_plus"] = lambda u: quote_plus(u or "")
app.jinja_env.filters["debug"] = lambda u: print(u) or u
app.jinja_env.filters["parse_int"] = lambda val: parse_int(val)


if Configuration().webserver["logging"]:
    logging.basicConfig(
        filename="../kemono.log",
        level=logging.getLevelName(Configuration().webserver["logging"]),
    )
    logging.getLogger("PIL").setLevel(logging.INFO)
    logging.getLogger("requests").setLevel(logging.WARNING)
    logging.getLogger("urllib3").setLevel(logging.WARNING)
    logging.getLogger("urllib3.connectionpool").setLevel(logging.ERROR)

if Configuration().sentry_dsn:
    import sentry_sdk
    from sentry_sdk.integrations.flask import FlaskIntegration
    from sentry_sdk.integrations.redis import RedisIntegration

    sentry_sdk.utils.MAX_STRING_LENGTH = 2048
    sentry_sdk.serializer.MAX_EVENT_BYTES = 10**7
    sentry_sdk.serializer.MAX_DATABAG_DEPTH = 8
    sentry_sdk.serializer.MAX_DATABAG_BREADTH = 20
    sentry_sdk.init(
        integrations=[FlaskIntegration(), RedisIntegration()],
        dsn=Configuration().sentry_dsn,
        release=os.getenv("GIT_COMMIT_HASH") or "NOT_FOUND",
        max_request_body_size="always",
        max_value_length=1024 * 4,
        attach_stacktrace=True,
        max_breadcrumbs=1000,
        send_default_pii=True,
    )

import src.internals.cache.redis as redis
import src.internals.database.database as database

database.init()
redis.init()

if exists(nondynamic_folder := join(Configuration().webserver["template_folder"].replace("..", "."), "nondynamic")):

    def nondynamic_view(page_file):
        def nondynamic_view_func():
            if ".html" in page_file:
                return make_response(
                    render_template(
                        f"nondynamic/{page_file}",
                        props=dict(),
                        base=request.args.to_dict(),
                    ),
                    200,
                )
            else:
                return send_from_directory(nondynamic_folder, page_file)

        return nondynamic_view_func

    for page in listdir(nondynamic_folder):
        app.get(f"/{splitext(page)[0]}", endpoint=f"get_{splitext(page)[0]}")(nondynamic_view(page))


@app.before_request
def do_init_stuff():
    app.permanent_session_lifetime = timedelta(days=3650)

    g.page_data = {}
    g.request_start_time = datetime.datetime.now()
    g.freesites = freesites
    g.artists_or_creators = Configuration().webserver["ui"]["config"]["artists_or_creators"]
    g.paysite_list = Configuration().webserver["ui"]["config"]["paysite_list"]

    g.paysites = paysites
    g.origin = Configuration().webserver["site"]
    g.custom_links = Configuration().webserver["ui"]["sidebar_items"]
    g.custom_footer = Configuration().webserver["ui"]["footer_items"]

    # Matomo.
    g.matomo_enabled = Configuration().webserver["ui"]["matomo"]["enabled"]
    g.matomo_plain_code = Configuration().webserver["ui"]["matomo"]["plain_code"]
    g.matomo_domain = Configuration().webserver["ui"]["matomo"]["tracking_domain"]
    g.matomo_code = Configuration().webserver["ui"]["matomo"]["tracking_code"]
    g.matomo_site_id = Configuration().webserver["ui"]["matomo"]["site_id"]

    # navbar
    g.disable_filehaus = Configuration().webserver["ui"]["sidebar"]["disable_filehaus"]
    g.disable_faq = Configuration().webserver["ui"]["sidebar"]["disable_faq"]
    g.disable_dms = Configuration().webserver["ui"]["sidebar"]["disable_dms"]

    # Ads.
    g.header_ad = Configuration().webserver["ui"]["ads"]["header"]
    g.middle_ad = Configuration().webserver["ui"]["ads"]["middle"]
    g.footer_ad = Configuration().webserver["ui"]["ads"]["footer"]
    g.slider_ad = Configuration().webserver["ui"]["ads"]["slider"]
    g.video_ad = Configuration().webserver["ui"]["ads"]["video"]

    # Banners.
    g.banner_global = Configuration().webserver["ui"]["banner"]["global"]
    g.banner_welcome = Configuration().webserver["ui"]["banner"]["welcome"]

    # Branding path prepend
    g.icons_prepend = Configuration().webserver["ui"]["files_url_prepend"]["icons_base_url"]
    g.banners_prepend = Configuration().webserver["ui"]["files_url_prepend"]["banners_base_url"]
    g.thumbnails_prepend = Configuration().webserver["ui"]["files_url_prepend"]["thumbnails_base_url"]

    g.mascot_path = Configuration().webserver["ui"]["home"]["mascot_path"]
    g.logo_path = Configuration().webserver["ui"]["home"]["logo_path"]
    g.welcome_credits = Configuration().webserver["ui"]["home"]["welcome_credits"]
    g.home_background_image = Configuration().webserver["ui"]["home"]["home_background_image"]
    g.announcements = Configuration().webserver["ui"]["home"]["announcements"]
    g.site_name = Configuration().webserver["ui"]["home"]["site_name"]
    g.canonical_url = urljoin(Configuration().webserver["site"], request.path)

    session.permanent = True
    session.modified = False

    if account := load_account():
        g.account = account
        if Configuration().enable_notifications:
            g.new_notifications_count = count_new_notifications(g.account.id)


@app.after_request
def do_finish_stuff(response):
    if not url_is_for_non_logged_file_extension(request.path):
        start_time = g.request_start_time
        end_time = datetime.datetime.now()
        elapsed = end_time - start_time
        app.logger.debug(
            f"[{end_time.strftime("%Y-%m-%d %X")}] "
            f"Completed {request.method} request to {request.url} "
            f"in {elapsed.microseconds / 1000}ms"
        )
    response.autocorrect_location_header = False
    return response


# adding the handlers there because
# flask doesn't allow handling 404/405 in blueprints
# https://flask.palletsprojects.com/en/2.3.x/errorhandling/#blueprint-error-handlers
@app.errorhandler(404)
def route_not_found(error):
    if request.path.startswith('/api/v1'):
        return jsonify(error="Not Found"), 404

    if request.path.startswith('/api/v2'):
        error = TDAPIError(type="http_error", message="Not Found")
        return create_api_v2_client_error_response(error, 404)

    return error, 404

@app.errorhandler(405)
def method_not_allowed(error):
    if request.path.startswith('/api/v1'):
        return jsonify(error="Method Not Allowed"), 405

    if request.path.startswith('/api/v2'):
        error = TDAPIError(type="http_error", message="Method Not Allowed")
        return create_api_v2_client_error_response(error, 405)

    return error, 405

@app.errorhandler(413)
def upload_exceeded(error):
    props = {"redirect": request.headers.get("Referer") if request.headers.get("Referer") else "/"}
    limit = int(getenv("REQUESTS_IMAGES")) if getenv("REQUESTS_IMAGES") else 1048576
    props["message"] = f"Submitted file exceeds the upload limit. {limit / 1024 / 1024} MB for requests images."
    return render_template("error.html", props=props), 413


@app.teardown_appcontext
def close(e):
    # removing account just in case
    g.pop("account", None)
    cursor = g.pop("cursor", None)
    if cursor is not None:
        cursor.close()
    connection = g.pop("connection", None)
    if connection is not None:
        try:
            pool = database.get_pool()
            if not connection.autocommit:
                connection.commit()
            pool.putconn(connection)
        except Exception:
            pass
