import asyncio
import logging
import sys
import tvmauth

from aiohttp import web
from typing import Optional

from smb.common.multiruntime.lib.io import setup_filesystem

from smb.common.aiotvm import TvmClient
from maps_adv.common.geoproduct import GeoproductClient
from maps_adv.config_loader import Config
from maps_adv.warden.client.lib import PeriodicalTask, TaskMaster

from . import api
from .core.async_xmlrpc_client import XmlRpcClient
from .core.balance_client import BalanceClient
from .data_manager import ClientsDataManager, OrdersDataManager, ProductsDataManager
from .db.engine import DB
from .domain import ClientsDomain, OrdersDomain, ProductsDomain
from .logging import JsonLoggingFormatter

setup_filesystem("maps_adv/billing_proxy/")


class Application:
    api: web.Application = None

    def __init__(self, config: Config):
        self.config = config

        self.clients_domain = None
        self.orders_domain = None
        self.orders_dm = None
        self.products_dm = None

    async def setup(self, db: DB):
        self._init_logging()

        tvm_client = await TvmClient(
            f"http://localhost:{self.config['TVMTOOL_PORT']}",
            self.config["TVMTOOL_LOCAL_AUTHTOKEN"],
        )
        xmlrpc_client = XmlRpcClient(
            host=self.config["BALANCE_XMLRPC_API_HOST"],
            port=self.config["BALANCE_XMLRPC_API_PORT"],
            path=self.config["BALANCE_XMLRPC_API_PATH"],
            use_https=True,
            tvm_client=tvm_client,
            tvm_destination="balance",
        )
        balance_client = BalanceClient(
            default_service_id=self.config["BALANCE_SERVICE_ID"],
            service_tokens={
                self.config["BALANCE_SERVICE_ID"]: self.config["BALANCE_SERVICE_TOKEN"]
            },
            xmlrpc_client=xmlrpc_client,
            operator_uid=self.config["BALANCE_OPERATOR_UID"],
        )
        geoproduct_client = await GeoproductClient(
            url=self.config["GEOPRODUCT_URL"],
            default_uid=self.config["GEOPRODUCT_DEFAULT_UID"],
            tvm_client=tvm_client,
            tvm_destination="geoproduct",
        )

        clients_dm = ClientsDataManager(db=db)
        orders_dm = OrdersDataManager(
            db=db,
            balance_client=balance_client,
            geoproduct_client=geoproduct_client,
            geoproduct_operator_id=self.config["BALANCE_OPERATOR_UID"],
            skip_balance_api_call_on_orders_charge=self.config[
                "SKIP_BALANCE_API_CALL_ON_ORDERS_CHARGE"
            ],
            use_recalculate_statistic_mode=self.config[
                "USE_RECALCULATE_STATISTIC_MODE"
            ],
            yt_cluster=self.config["YT_CLUSTER"],
            yt_token=self.config["YT_TOKEN"],
            reconciliation_report_dir=self.config["YT_RECOINCILIATION_REPORT_DIR"],
        )
        products_dm = ProductsDataManager(db=db)

        self.clients_domain = ClientsDomain(clients_dm, balance_client)
        self.orders_domain = OrdersDomain(
            orders_dm,
            clients_dm,
            products_dm,
            balance_client=balance_client,
            balance_service_id=self.config["BALANCE_SERVICE_ID"],
            geoprod_service_id=self.config["GEOPROD_SERVICE_ID"],
            seasonal_coefs_since=self.config["SEASONAL_COEFS_SINCE"],
        )
        self.products_domain = ProductsDomain(
            products_dm,
            clients_dm,
            balance_service_id=self.config["BALANCE_SERVICE_ID"],
            seasonal_coefs_since=self.config["SEASONAL_COEFS_SINCE"],
        )

        tvmport = self.config["TVMTOOL_PORT"]

        self.api = api.create(
            clients_domain=self.clients_domain,
            orders_domain=self.orders_domain,
            products_domain=self.products_domain,
            tvm_client=tvmauth.TvmClient(
                tvmauth.TvmToolClientSettings(
                    "billing-proxy",
                    auth_token=self.config["TVMTOOL_LOCAL_AUTHTOKEN"],
                    port=tvmport,
                )
            ),
        )

        self._setup_tasks()

        self.api.on_shutdown.append(
            lambda _: asyncio.wait(
                [geoproduct_client.close(), xmlrpc_client.close(), tvm_client.close()]
            )
        )

    def _init_logging(self):
        # Send logs to stdout encoded as JSON for qloud to gather it
        handler = logging.StreamHandler(sys.stdout)
        handler.setFormatter(JsonLoggingFormatter())

        root_logger = logging.getLogger("billing_proxy")
        root_logger.propagate = False
        root_logger.addHandler(handler)

    def _setup_tasks(self):
        if not self.config["WARDEN_URL"]:
            return

        tasks = [
            # Sync clients and contracts data with Balance
            PeriodicalTask(
                "billing_sync_client_data",
                lambda *_: self.clients_domain.sync_clients_data_and_contracts_with_balance(),  # noqa: E501
                logger=logging.getLogger(
                    "billing_proxy.PeriodicalTask.billing_sync_client_data"
                ),
            ),
            # Generates reconciliation report for geoprod orders
            PeriodicalTask(
                "billing__generate_geoprod_reconciliation_report",
                lambda *_: self.orders_domain.load_geoprod_reconciliation_report(),  # noqa: E501
                logger=logging.getLogger(
                    "billing_proxy.PeriodicalTask.billing__generate_geoprod_reconciliation_report"
                ),
            ),
        ]

        self._task_master = TaskMaster(
            server_url=self.config["WARDEN_URL"],
            tasks=tasks,
            logger=logging.getLogger("billing_proxy.TaskMaster"),
        )

        self.api.on_startup.append(lambda *_: self._task_master.run())
        self.api.on_shutdown.append(lambda *_: self._task_master.stop())

    async def _run(self) -> web.Application:
        db = await DB.create(self.config["DATABASE_URL"])
        await self.setup(db)
        self.api.on_shutdown.append(lambda app: asyncio.wait([db.close()]))

        return self.api

    def run(self, host: Optional[str] = None, port: Optional[int] = None):
        web.run_app(self._run(), host=host, port=port, loop=asyncio.get_event_loop())
