import csv
import logging
from datetime import datetime
from pathlib import Path
from typing import Type

from dateparser import parse

from monopoly.banks import BankBase
from monopoly.config import DateOrder
from monopoly.generic import GenericStatementHandler
from monopoly.generic.generic_handler import GenericBank
from monopoly.handler import StatementHandler
from monopoly.metadata import MetadataAnalyzer
from monopoly.pdf import PdfParser
from monopoly.statements import CreditStatement, DebitStatement
from monopoly.statements.transaction import Transaction
from monopoly.write import generate_name

logger = logging.getLogger(__name__)


class Pipeline:
    """Handles extract, transform and load (ETL) logic for bank statements"""

    def __init__(self, file_path: Path):
        self.file_path = file_path
        self.bank = self._detect_bank()
        self.parser = self._create_parser()
        self.handler = self._create_handler()
        self.statement = self.handler.get_statement(self.parser)

    def _create_parser(self) -> PdfParser:
        return PdfParser(self.bank, file_path=self.file_path)

    def _create_handler(self) -> GenericStatementHandler | StatementHandler:
        if issubclass(self.bank, GenericBank):
            logger.info("Using generic statement handler")
            return GenericStatementHandler(self.parser)
        logger.info("Using statement handler with bank: %s", self.bank.__name__)
        return StatementHandler(self.parser)

    def _detect_bank(self) -> Type[BankBase]:
        analyzer = MetadataAnalyzer(self.file_path)
        if bank := analyzer.bank:
            return bank
        logger.warning("Unable to detect bank, transactions may be inaccurate")
        return GenericBank

    def extract(self, safety_check=True) -> CreditStatement | DebitStatement:
        """Extracts transactions from the statement, and performs
        a safety check to make sure that total transactions add up"""
        if not self.statement.transactions:
            raise ValueError("No transactions found - statement extraction failed")

        if not self.statement.statement_date:
            raise ValueError("No statement date found")

        if safety_check:
            self.statement.perform_safety_check()

        return self.statement

    @staticmethod
    def transform(
        transactions: list[Transaction],
        statement_date: datetime,
        transaction_date_order: DateOrder,
    ) -> list[Transaction]:
        logger.debug("Running transformation functions on DataFrame")

        def convert_date(transaction: Transaction, transaction_date_order: DateOrder):
            """
            Converts each date to a ISO 8601 (YYYY-MM-DD) format.

            Implements cross-year logic by attributing transactions from
            October, November, and December to the previous year if
            the statement month is January.
            e.g. if the statement month is Jan/Feb 2024, transactions from
            Oct/Nov/Dec should be attributed to the previous year.
            """
            transaction.date += f" {statement_date.year}"
            parsed_date = parse(
                transaction.date,
                settings=transaction_date_order.settings,
            )
            if parsed_date:
                if statement_date.month in (1, 2) and parsed_date.month > 2:
                    parsed_date = parsed_date.replace(year=parsed_date.year - 1)

                return parsed_date.isoformat()[:10]
            raise RuntimeError("Could not convert date")

        logger.debug("Transforming dates to ISO 8601")

        for transaction in transactions:
            transaction.date = convert_date(transaction, transaction_date_order)
        return transactions

    @staticmethod
    def load(
        transactions: list[Transaction],
        statement: CreditStatement | DebitStatement,
        output_directory: Path,
    ):
        if isinstance(output_directory, str):
            output_directory = Path(output_directory)

        filename = generate_name(
            document=statement.document,
            format_type="file",
            statement_config=statement.config,
            statement_type=statement.statement_type,
            statement_date=statement.statement_date,
        )

        output_path = output_directory / filename
        logger.debug("Writing CSV to file path: %s", output_path)

        with open(output_path, mode="w", encoding="utf8") as file:
            writer = csv.writer(file)

            # header
            writer.writerow(statement.columns)

            for transaction in transactions:
                writer.writerow(
                    (
                        [
                            transaction.date,
                            transaction.description,
                            transaction.amount,
                        ]
                    )
                )

        return output_path
