"""
Settings business logic services.
"""
from typing import Optional
from fastapi import HTTPException, UploadFile
from sqlalchemy.orm import Session
from sqlalchemy import select, func

from src.apps.settings import crud
from src.apps.settings.defaults import MERCHANT_SETTINGS_DEFAULTS, SETTINGS_GROUP_MAP, ALL_SETTINGS_GROUPS
from src.apps.merchants.models.merchant import Merchant
from src.apps.merchants.models.merchant_contacts import MerchantContacts
from src.apps.base.models.address import Address
from src.apps.merchants.models.merchant_discount import MerchantDiscount
from src.apps.settings.models.user_preferences import UserPreferences


# ---- VT SETTINGS ----

VT_GROUP_VALIDATIONS = {
    "auth": ["auth_pre_auth", "auth_request_auth"],
    "details": ["details_quick_charge", "details_item_details"],
    "frequency": ["freq_single", "freq_split", "freq_recurring"],
    "method": ["method_card", "method_ach", "method_cash", "method_cheque", "method_split_tender"],
    "receipt": ["receipt_email", "receipt_sms"],
}


def get_group_settings(group: str, merchant_id: int, db: Session) -> dict:
    """Fetch all KV rows for a group, merged with defaults for missing keys."""
    rows = crud.get_settings_by_group(group, merchant_id, db)
    row_map = {r.key: r.value for r in rows}
    defaults = MERCHANT_SETTINGS_DEFAULTS.get(group, {})
    merged = {}
    for key, default_value in defaults.items():
        merged[key] = row_map.get(key, default_value)
    return merged


def update_group_settings(group: str, updates: dict, merchant_id: int, db: Session) -> dict:
    """Bulk upsert settings for a group. Returns the updated settings dict.

    Only keys present in MERCHANT_SETTINGS_DEFAULTS for the group are written;
    unknown keys are silently dropped to prevent arbitrary key injection.
    """
    allowed_keys = set(MERCHANT_SETTINGS_DEFAULTS.get(group, {}).keys())
    for key, value in updates.items():
        if key not in allowed_keys:
            continue  # reject unknown keys — do not store arbitrary settings
        crud.upsert_setting(group=group, key=key, value=str(value), merchant_id=merchant_id, db=db)
    return get_group_settings(group, merchant_id, db)


def validate_vt_settings(updates: dict, merchant_id: int, db: Session) -> None:
    """Raises HTTPException(422) if any VT group would have all options disabled.

    Merges the incoming patch against the *current persisted state* (not just
    the patch itself) so that incremental disabling — spreading the disable
    across multiple requests — is caught correctly.
    """
    current = get_vt_settings(merchant_id, db)
    # Build the merged post-update view: current values overridden by the patch.
    # Incoming updates arrive as booleans at this point; current values are already booleans.
    merged = {k: v for k, v in current.items()}
    for k, v in updates.items():
        if k in merged:
            merged[k] = bool(v) if not isinstance(v, bool) else v

    for group_name, keys in VT_GROUP_VALIDATIONS.items():
        group_values = [merged.get(k, True) for k in keys]
        if not any(group_values):
            raise HTTPException(
                status_code=422,
                detail=f"At least one {group_name} option must remain enabled."
            )


def _convert_vt_dict(raw: dict) -> dict:
    """Convert string 'true'/'false' values to booleans for VT settings."""
    bool_keys = {
        "auth_pre_auth", "auth_request_auth", "details_quick_charge", "details_item_details",
        "freq_single", "freq_split", "freq_recurring", "method_card", "method_ach",
        "method_cash", "method_cheque", "method_split_tender",
        "enable_attachments", "enable_customer_message", "enable_discounts",
        "receipt_email", "receipt_sms",
    }
    result = {}
    for k, v in raw.items():
        if k in bool_keys:
            result[k] = str(v).lower() == "true"
        else:
            result[k] = v
    return result


def get_vt_settings(merchant_id: int, db: Session) -> dict:
    raw = get_group_settings("vt_settings", merchant_id, db)
    return _convert_vt_dict(raw)


def update_vt_settings(updates: dict, merchant_id: int, db: Session) -> dict:
    validate_vt_settings(updates, merchant_id, db)
    str_updates = {
        k: ("true" if v is True else "false" if v is False else str(v))
        for k, v in updates.items()
    }
    update_group_settings("vt_settings", str_updates, merchant_id, db)
    return get_vt_settings(merchant_id, db)


# ---- HPP SETTINGS ----

def _convert_hpp_dict(raw: dict) -> dict:
    bool_keys = {"display_payer_ip", "display_payer_location"}
    result = {}
    for k, v in raw.items():
        if k in bool_keys:
            result[k] = str(v).lower() == "true"
        else:
            result[k] = v
    return result


def get_hpp_settings(merchant_id: int, db: Session) -> dict:
    raw = get_group_settings("hpp_settings", merchant_id, db)
    return _convert_hpp_dict(raw)


def update_hpp_settings(updates: dict, merchant_id: int, db: Session) -> dict:
    str_updates = {
        k: ("true" if v is True else "false" if v is False else str(v))
        for k, v in updates.items()
    }
    update_group_settings("hpp_settings", str_updates, merchant_id, db)
    return get_hpp_settings(merchant_id, db)


# ---- INVOICE & RECEIPT SETTINGS ----

def _convert_bool_dict(raw: dict, bool_keys: set) -> dict:
    result = {}
    for k, v in raw.items():
        result[k] = str(v).lower() == "true" if k in bool_keys else v
    return result


def get_invoice_settings(merchant_id: int, db: Session) -> dict:
    raw = get_group_settings("invoice_settings", merchant_id, db)
    return _convert_bool_dict(
        raw,
        {"show_item_description", "show_customer_name", "show_transaction_time", "show_invoice_number"},
    )


def update_invoice_settings(updates: dict, merchant_id: int, db: Session) -> dict:
    str_updates = {
        k: ("true" if v is True else "false" if v is False else str(v))
        for k, v in updates.items()
    }
    update_group_settings("invoice_settings", str_updates, merchant_id, db)
    return get_invoice_settings(merchant_id, db)


def get_receipt_settings(merchant_id: int, db: Session) -> dict:
    raw = get_group_settings("receipt_settings", merchant_id, db)
    return _convert_bool_dict(
        raw,
        {"show_item_description", "show_customer_name", "show_transaction_time", "show_receipt_number"},
    )


def update_receipt_settings(updates: dict, merchant_id: int, db: Session) -> dict:
    str_updates = {
        k: ("true" if v is True else "false" if v is False else str(v))
        for k, v in updates.items()
    }
    update_group_settings("receipt_settings", str_updates, merchant_id, db)
    return get_receipt_settings(merchant_id, db)


# ---- BRANDING ----

BRANDING_GROUPS = ["Logo", "Colors", "Business URLs"]


def get_branding_settings(merchant_id: int, db: Session) -> dict:
    result = {}
    for group in BRANDING_GROUPS:
        raw = get_group_settings(group, merchant_id, db)
        result[group] = raw
    return result


def update_branding_settings(updates: dict, merchant_id: int, db: Session) -> dict:
    """Updates is a flat dict of key→value. Group is inferred from defaults."""
    key_to_group = {}
    for group, keys in MERCHANT_SETTINGS_DEFAULTS.items():
        if group in BRANDING_GROUPS:
            for key in keys:
                key_to_group[key] = group
    for key, value in updates.items():
        group = key_to_group.get(key)
        if group:
            str_value = str(value).lower() if isinstance(value, bool) else str(value)
            crud.upsert_setting(group=group, key=key, value=str_value, merchant_id=merchant_id, db=db)
    return get_branding_settings(merchant_id, db)


async def upload_logo(merchant_id: int, user_id: int, file: UploadFile, db: Session) -> str:
    """Upload merchant logo. Validates file, uploads via file_services, updates merchant_settings."""
    import io
    from src.apps.merchants.models.merchant_settings import MerchantSettings
    from sqlalchemy import select as sa_select

    # SVG is intentionally excluded: SVG files can embed <script> tags and
    # are a stored-XSS vector when served without a restrictive Content-Security-Policy.
    # Only raster formats whose content can be verified via magic bytes are allowed.
    ALLOWED_MIMES = {"image/png", "image/jpeg", "image/jpg"}
    MAX_SIZE = 2 * 1024 * 1024  # 2MB

    if file.content_type not in ALLOWED_MIMES:
        raise HTTPException(
            status_code=400,
            detail="Logo must be a raster image (PNG or JPG).",
        )

    content = await file.read()
    if len(content) > MAX_SIZE:
        raise HTTPException(status_code=400, detail="Logo file size must not exceed 2MB.")

    # --- Magic-byte validation ---
    # Validate actual file bytes regardless of client-supplied Content-Type header
    # to prevent MIME spoofing (e.g. an executable uploaded with Content-Type: image/png).
    PNG_MAGIC = b"\x89PNG\r\n\x1a\n"
    JPEG_MAGIC = b"\xff\xd8\xff"

    declared_mime = file.content_type

    if declared_mime == "image/png":
        if not content.startswith(PNG_MAGIC):
            raise HTTPException(status_code=400, detail="File content does not match declared image type (PNG).")
    elif declared_mime in {"image/jpeg", "image/jpg"}:
        if not content.startswith(JPEG_MAGIC):
            raise HTTPException(status_code=400, detail="File content does not match declared image type (JPEG).")

    # Reset file position
    file.file.seek(0)

    # For PNG/JPEG, validate aspect ratio
    if file.content_type in {"image/png", "image/jpeg", "image/jpg"}:
        try:
            from PIL import Image
            img_bytes = io.BytesIO(content)
            img = Image.open(img_bytes)
            width, height = img.size
            if height > 0:
                ratio = width / height
                if not (0.25 <= ratio <= 4.0):
                    raise HTTPException(
                        status_code=400,
                        detail=f"Logo aspect ratio {ratio:.2f} is outside allowed range (0.25 to 4.0).",
                    )
        except HTTPException:
            raise
        except Exception:
            pass  # If resolution check fails, proceed anyway

    # Get current logo_file_id to delete old file
    old_file_id_val = None
    stmt = sa_select(MerchantSettings).where(
        MerchantSettings.merchant_id == merchant_id,
        MerchantSettings.group == "Logo",
        MerchantSettings.key == "logo_file_id",
    )
    old_setting = db.execute(stmt).scalar_one_or_none()
    if old_setting and old_setting.value:
        try:
            old_file_id_val = int(old_setting.value)
        except (ValueError, TypeError):
            pass

    # Fetch merchant and user objects
    merchant_obj = db.execute(
        sa_select(Merchant).where(Merchant.id == merchant_id)
    ).scalar_one_or_none()
    if not merchant_obj:
        raise HTTPException(status_code=404, detail="Merchant not found.")

    from src.apps.users.models.user import User
    user_obj = db.execute(
        sa_select(User).where(User.id == user_id)
    ).scalar_one_or_none()

    # Upload via existing file service
    from src.apps.files.file_services import upload_single_file
    file_response = await upload_single_file(
        db=db, file=file, file_type="logo", created_by=user_obj, merchant=merchant_obj
    )

    logo_url = file_response.full_url if hasattr(file_response, "full_url") else str(file_response)
    file_id = file_response.id if hasattr(file_response, "id") else None

    # Update merchant_settings
    crud.upsert_setting("Logo", "logo_url", logo_url, merchant_id, db, label="Logo URL")
    if file_id:
        crud.upsert_setting("Logo", "logo_file_id", str(file_id), merchant_id, db, label="Logo File ID")

    # Delete old file if exists
    if old_file_id_val:
        try:
            from src.apps.files.file_services import delete_file_by_id
            await delete_file_by_id(db, old_file_id_val, user_obj)
        except Exception:
            pass  # Don't fail the upload if cleanup fails

    return logo_url


# ---- USER PREFERENCES ----

def get_user_preferences(user_id: int, db: Session) -> UserPreferences:
    return crud.get_or_create_preferences(user_id, db)


def update_user_preferences(user_id: int, updates: dict, db: Session) -> UserPreferences:
    return crud.update_preferences(user_id, updates, db)


# ---- ACCOUNT SUMMARY ----

def get_account_summary(merchant_id: int, db: Session) -> dict:
    merchant = db.execute(select(Merchant).where(Merchant.id == merchant_id)).scalar_one_or_none()
    if not merchant:
        raise HTTPException(status_code=404, detail="Merchant not found.")

    # Get active provider
    active_provider = None
    try:
        from src.apps.payment_providers.models.merchant_provider_config import MerchantProviderConfig
        from src.apps.payment_providers.models.payment_provider import PaymentProvider
        config = db.execute(
            select(MerchantProviderConfig).where(
                MerchantProviderConfig.merchant_id == merchant_id,
                MerchantProviderConfig.is_active == True,
            )
        ).scalar_one_or_none()
        if config:
            provider = db.execute(
                select(PaymentProvider).where(PaymentProvider.id == config.provider_id)
            ).scalar_one_or_none()
            if provider:
                active_provider = provider.slug
    except Exception:
        pass

    return {
        "id": merchant.id,
        "uin": merchant.uin,
        "name": merchant.name,
        "email": merchant.email,
        "phone": merchant.phone,
        "is_active": merchant.is_active,
        "active_provider": active_provider,
    }


# ---- CONTACTS ----

def list_contacts(merchant_id: int, db: Session, page: int = 1, per_page: int = 10, search: Optional[str] = None):
    return crud.list_contacts(merchant_id, db, page=page, per_page=per_page, search=search)


def create_contact(merchant_id: int, data: dict, db: Session):
    return crud.create_contact(merchant_id, data, db)


def update_contact(contact_id: int, merchant_id: int, data: dict, db: Session):
    contact = crud.update_contact(contact_id, merchant_id, data, db)
    if not contact:
        raise HTTPException(status_code=404, detail="Contact not found.")
    return contact


def delete_contact(contact_id: int, merchant_id: int, db: Session):
    if not crud.soft_delete_contact(contact_id, merchant_id, db):
        raise HTTPException(status_code=404, detail="Contact not found.")


_ADDRESS_FIELDS = {"address_line_1", "address_line_2", "zipcode", "city", "state", "country"}


# ---- LOCATIONS ----

def list_locations(merchant_id: int, db: Session, page: int = 1, per_page: int = 10, search: Optional[str] = None):
    return crud.list_locations(merchant_id, db, page=page, per_page=per_page, search=search)


def create_location(merchant_id: int, data: dict, db: Session):
    address_data = {k: v for k, v in data.items() if k in _ADDRESS_FIELDS}
    location_data = {k: v for k, v in data.items() if k not in _ADDRESS_FIELDS}
    location = crud.create_location(merchant_id, location_data, db)
    if address_data:
        address = Address(merchant_id=merchant_id, **address_data)
        db.add(address)
        db.flush()
        location.address_id = address.id
        db.flush()
    return location


def update_location(location_id: int, merchant_id: int, data: dict, db: Session):
    address_data = {k: v for k, v in data.items() if k in _ADDRESS_FIELDS}
    location_data = {k: v for k, v in data.items() if k not in _ADDRESS_FIELDS}
    location = crud.update_location(location_id, merchant_id, location_data, db)
    if not location:
        raise HTTPException(status_code=404, detail="Location not found.")
    if address_data:
        if location.address_id:
            address = db.get(Address, location.address_id)
            if address:
                for field, value in address_data.items():
                    setattr(address, field, value)
                db.flush()
        else:
            address = Address(merchant_id=merchant_id, **address_data)
            db.add(address)
            db.flush()
            location.address_id = address.id
            db.flush()
    return location


def delete_location(location_id: int, merchant_id: int, db: Session):
    if not crud.soft_delete_location(location_id, merchant_id, db):
        raise HTTPException(status_code=404, detail="Location not found.")


# ---- DISCOUNTS ----

def list_discounts(merchant_id: int, search: Optional[str], is_active: Optional[bool], db: Session):
    return crud.list_discounts(merchant_id, search, is_active, db)


def create_discount(merchant_id: int, user_id: int, data: dict, db: Session):
    return crud.create_discount(merchant_id, user_id, data, db)


def update_discount(discount_id: int, merchant_id: int, data: dict, db: Session):
    discount = crud.update_discount(discount_id, merchant_id, data, db)
    if not discount:
        raise HTTPException(status_code=404, detail="Discount not found.")
    return discount


def delete_discount(discount_id: int, merchant_id: int, db: Session):
    if not crud.soft_delete_discount(discount_id, merchant_id, db):
        raise HTTPException(status_code=404, detail="Discount not found.")


# ---- SEEDER / RESET ----

def seed_for_merchant(
    merchant_id: int,
    groups: Optional[list],
    overwrite: bool,
    db: Session,
) -> dict:
    return crud.seed_settings_for_merchant(merchant_id, groups, overwrite, db)


def seed_all_merchants(db: Session) -> dict:
    from src.apps.merchants.models.merchant_settings import MerchantSettings
    merchants = db.execute(select(Merchant)).scalars().all()
    seeded_count = 0
    for merchant in merchants:
        existing = db.execute(
            select(func.count(MerchantSettings.id)).where(
                MerchantSettings.merchant_id == merchant.id
            )
        ).scalar()
        if existing == 0:
            crud.seed_settings_for_merchant(merchant.id, None, False, db)
            seeded_count += 1
    return {"merchants_seeded": seeded_count, "total_merchants": len(merchants)}


def reset_settings(merchant_id: int, groups: list, db: Session) -> dict:
    """Reset specified group slugs to defaults. Returns new settings dict keyed by group slug."""
    unknown = [g for g in groups if g not in SETTINGS_GROUP_MAP]
    if unknown:
        raise HTTPException(
            status_code=422,
            detail=f"Unknown settings groups: {unknown}. Valid: {list(SETTINGS_GROUP_MAP.keys())}",
        )

    result = {}
    for slug in groups:
        db_groups = SETTINGS_GROUP_MAP[slug]
        if slug == "branding":
            # Don't clear Logo group (preserve logo_url)
            crud.delete_settings_by_groups(["Colors", "Business URLs"], merchant_id, db)
            for group in ["Colors", "Business URLs"]:
                for key, value in MERCHANT_SETTINGS_DEFAULTS.get(group, {}).items():
                    crud.upsert_setting(group, key, value, merchant_id, db)
        else:
            crud.delete_settings_by_groups(db_groups, merchant_id, db)
            for group in db_groups:
                for key, value in MERCHANT_SETTINGS_DEFAULTS.get(group, {}).items():
                    crud.upsert_setting(group, key, value, merchant_id, db)
        result[slug] = {group: get_group_settings(group, merchant_id, db) for group in db_groups}
    return result


# ---- CHECKOUT SETTINGS ----

def get_checkout_settings(merchant_id: int, db: Session) -> dict:
    """
    Return the merchant's CheckoutSettings row as a dict.
    If no settings row exists, return defaults.
    """
    from src.apps.checkouts import crud as checkout_crud

    row = checkout_crud.get_checkout_settings(db, merchant_id)
    if row is None:
        return {
            "merchant_id": merchant_id,
            "default_tip_enabled": False,
            "default_tip_percentages": None,
            "default_thank_you_message": None,
            "default_redirect_url": None,
            "default_require_payer_verification": False,
            "default_payer_verification_type": None,
            "created_at": None,
            "updated_at": None,
        }
    return {
        "merchant_id": row.merchant_id,
        "default_tip_enabled": row.default_tip_enabled,
        "default_tip_percentages": row.default_tip_percentages,
        "default_thank_you_message": row.default_thank_you_message,
        "default_redirect_url": row.default_redirect_url,
        "default_require_payer_verification": row.default_require_payer_verification,
        "default_payer_verification_type": row.default_payer_verification_type,
        "created_at": row.created_at,
        "updated_at": row.updated_at,
    }


def update_checkout_settings(updates: dict, merchant_id: int, db: Session) -> dict:
    """
    Upsert the merchant's CheckoutSettings row from a partial updates dict.
    """
    from src.apps.checkouts import crud as checkout_crud

    checkout_crud.upsert_checkout_settings(db, merchant_id, updates)
    return get_checkout_settings(merchant_id, db)
