from __future__ import annotations

import csv
import io

from fastapi import HTTPException

from app.modules.clients import repository
from app.core.security.passwords import hash_password
from app.modules.clients.association_admin_service import (
    ensure_active_association,
    update_association_fields,
)
from app.modules.clients.constants import (
    CLIENT_CATEGORY_OPTIONS,
    CLIENT_STATUS,
    CLIENT_TYPES,
    RISK_CATEGORIES,
)


def _payload_from_schema(data):
    return data.model_dump(exclude_none=True) if hasattr(data, "model_dump") else data.dict(exclude_none=True)




def _ensure_portal_passwords(email: str | None, portal_password: str | None, portal_password_confirm: str | None, *, required: bool):
    email_clean = (email or '').strip().lower()
    pw = (portal_password or '').strip()
    pw2 = (portal_password_confirm or '').strip()
    if not required and not pw and not pw2:
        return email_clean, None
    if not email_clean:
        raise HTTPException(status_code=400, detail="Email is required to create the client frontend login.")
    if len(pw) < 8:
        raise HTTPException(status_code=400, detail="Portal password must be at least 8 characters.")
    if pw != pw2:
        raise HTTPException(status_code=400, detail="Portal password and confirm password do not match.")
    return email_clean, pw


def _sync_client_portal_user(db, *, row, portal_password: str | None = None, portal_password_confirm: str | None = None):
    email_clean, pw = _ensure_portal_passwords(
        getattr(row, 'email', None),
        portal_password,
        portal_password_confirm,
        required=bool(portal_password or portal_password_confirm or not getattr(row, 'portal_user_id', None)),
    )

    if not pw:
        return row

    existing_user = repository.get_user_by_email(db, email=email_clean, exclude_user_id=getattr(row, 'portal_user_id', None))
    if existing_user:
        raise HTTPException(status_code=400, detail="That email is already used by another login.")

    if row.portal_user_id:
        user = db.get(repository.User, int(row.portal_user_id))
        if not user:
            row = repository.update_client(db, row, {'portal_user_id': None})
        else:
            user.email = email_clean
            user.full_name = (row.client_name or '').strip()
            user.password_hash = hash_password(pw)
            user.tenant_id = row.tenant_id
            user.branch_id = row.branch_id
            user.is_active = True
            user.allow_login = True
            user.is_locked = False
            user.must_change_password = False
            db.add(user)
            db.commit()
            db.refresh(user)
            return row

    if not row.portal_user_id:
        user = repository.create_portal_user(
            db,
            email=email_clean,
            full_name=row.client_name,
            tenant_id=row.tenant_id,
            branch_id=row.branch_id,
            password=pw,
        )
        role = repository.get_role_by_name(db, 'Client')
        if not role:
            raise HTTPException(status_code=500, detail='Client role is not available.')
        repository.ensure_user_role(db, user_id=user.id, role_id=role.id)
        row = repository.update_client(db, row, {'portal_user_id': user.id})
    return row


def _validate_scope_for_create(data, scope, actor_user_id):
    if scope.own_only:
        data.partner_id = scope.locked_partner_id or actor_user_id
    if not scope.allow_cross_tenant:
        data.tenant_id = scope.tenant_id
    if not scope.allow_cross_branch and scope.branch_id:
        data.branch_id = scope.branch_id
    return data


def _validate_scope_for_edit(data, scope, actor_user_id, *, existing_row, current_user_roles):
    role_names = {str(r).lower() for r in current_user_roles}

    if data.tenant_id is None:
        data.tenant_id = existing_row.tenant_id
    if data.branch_id is None:
        data.branch_id = existing_row.branch_id
    if data.partner_id is None:
        data.partner_id = existing_row.partner_id

    if "partner" in role_names and data.partner_id and data.partner_id != actor_user_id:
        raise HTTPException(status_code=400, detail="Partner users cannot assign clients to another partner.")

    if "consultant" in role_names and data.partner_id and data.partner_id != existing_row.partner_id:
        raise HTTPException(status_code=400, detail="Consultants cannot assign or change partner mapping.")

    if "firm admin" in role_names:
        if existing_row.tenant_id != scope.tenant_id:
            raise HTTPException(status_code=403, detail="Firm Admin can only manage clients within own firm.")
        data.tenant_id = scope.tenant_id
        if not scope.allow_cross_branch and data.branch_id != scope.branch_id:
            raise HTTPException(status_code=403, detail="Branch change is not allowed in current scope.")
        return data

    if "system admin" in role_names:
        return data

    if scope.own_only:
        data.partner_id = scope.locked_partner_id or actor_user_id
        if existing_row.partner_id != actor_user_id:
            raise HTTPException(status_code=403, detail="You can only edit your own associated clients.")

    return data


def _write_association_from_client(db, client_row, *, actor_user_id, current_user_roles):
    roles = {str(r).lower() for r in current_user_roles}
    ensure_active_association(db, client_row.id)

    if "system admin" in roles:
        return update_association_fields(
            db,
            client_row.id,
            association_type="firm" if client_row.tenant_id else "self_service_unassigned",
            firm_tenant_id=client_row.tenant_id,
            partner_user_id=client_row.partner_id,
            created_source="system_admin",
        )

    if "firm admin" in roles:
        return update_association_fields(
            db,
            client_row.id,
            association_type="firm",
            firm_tenant_id=client_row.tenant_id,
            partner_user_id=client_row.partner_id,
            created_source="firm_admin",
        )

    if "partner" in roles:
        return update_association_fields(
            db,
            client_row.id,
            association_type="firm",
            firm_tenant_id=client_row.tenant_id,
            partner_user_id=actor_user_id,
            created_source="partner",
        )

    if "consultant" in roles:
        return update_association_fields(
            db,
            client_row.id,
            association_type="consultant",
            consultant_id=actor_user_id,
            created_source="consultant",
        )

    return update_association_fields(
        db,
        client_row.id,
        association_type="self_service_unassigned",
        created_source="self_service",
    )


def create_client_service(db, *, data, actor_user_id: int, scope, current_user_roles=None, portal_password: str | None = None, portal_password_confirm: str | None = None):
    current_user_roles = current_user_roles or []
    data = _validate_scope_for_create(data, scope, actor_user_id)

    existing = repository.get_client_by_code(db, tenant_id=data.tenant_id, client_code=data.client_code)
    if existing:
        raise HTTPException(status_code=400, detail="Client code already exists.")

    if data.pan:
        existing_pan = repository.get_client_by_pan(db, tenant_id=data.tenant_id, pan=data.pan)
        if existing_pan:
            raise HTTPException(status_code=400, detail="PAN already exists for another client in this tenant.")

    if data.gstin:
        existing_gstin = repository.get_client_by_gstin(db, tenant_id=data.tenant_id, gstin=data.gstin)
        if existing_gstin:
            raise HTTPException(status_code=400, detail="GSTIN already exists for another client in this tenant.")

    payload = _payload_from_schema(data)
    row = repository.create_client(db, payload)
    row = _sync_client_portal_user(db, row=row, portal_password=portal_password, portal_password_confirm=portal_password_confirm)
    _write_association_from_client(db, row, actor_user_id=actor_user_id, current_user_roles=current_user_roles)

    repository.write_audit_log(
        db,
        client_id=row.id,
        tenant_id=row.tenant_id,
        branch_id=row.branch_id,
        actor_user_id=actor_user_id,
        action="created",
        summary="Client created with association sync.",
        payload_json={"client_id": row.id, "partner_id": row.partner_id},
    )
    return row


def update_client_service(db, *, row, data, actor_user_id: int, scope, current_user_roles=None, portal_password: str | None = None, portal_password_confirm: str | None = None):
    current_user_roles = current_user_roles or []
    data = _validate_scope_for_edit(
        data,
        scope,
        actor_user_id,
        existing_row=row,
        current_user_roles=current_user_roles,
    )

    payload = _payload_from_schema(data)

    if payload.get("pan"):
        existing_pan = repository.get_client_by_pan(db, tenant_id=payload["tenant_id"], pan=payload["pan"])
        if existing_pan and existing_pan.id != row.id:
            raise HTTPException(status_code=400, detail="PAN already exists for another client in this tenant.")

    if payload.get("gstin"):
        existing_gstin = repository.get_client_by_gstin(db, tenant_id=payload["tenant_id"], gstin=payload["gstin"])
        if existing_gstin and existing_gstin.id != row.id:
            raise HTTPException(status_code=400, detail="GSTIN already exists for another client in this tenant.")

    updated = repository.update_client(db, row, payload)
    updated = _sync_client_portal_user(db, row=updated, portal_password=portal_password, portal_password_confirm=portal_password_confirm)
    _write_association_from_client(db, updated, actor_user_id=actor_user_id, current_user_roles=current_user_roles)

    repository.write_audit_log(
        db,
        client_id=updated.id,
        tenant_id=updated.tenant_id,
        branch_id=updated.branch_id,
        actor_user_id=actor_user_id,
        action="updated",
        summary="Client updated with association sync.",
        payload_json={"client_id": updated.id, "partner_id": updated.partner_id},
    )
    return updated


def get_client_or_404(
    db,
    *,
    client_id: int,
    tenant_id: int,
    branch_id: int | None,
    allow_cross_branch: bool,
    allow_all_clients: bool = False,
):
    row = repository.get_client_by_id(db, client_id)
    if not row:
        raise HTTPException(status_code=404, detail="Client not found.")
    if not allow_all_clients and row.tenant_id != tenant_id:
        raise HTTPException(status_code=404, detail="Client not found in current tenant.")
    if not allow_all_clients and not allow_cross_branch and branch_id and row.branch_id != branch_id:
        raise HTTPException(status_code=404, detail="Client not found in current branch.")
    return row


def list_clients_payload(db, **kwargs):
    return repository.list_clients(db, **kwargs)


def list_client_audit_logs(db, *, row, limit: int = 50):
    return repository.list_audit_logs(db, client_id=row.id, limit=limit)


def deactivate_client_service(db, *, row, actor_user_id: int):
    row = repository.update_client(db, row, {"status": "inactive"})
    repository.write_audit_log(
        db,
        client_id=row.id,
        tenant_id=row.tenant_id,
        branch_id=row.branch_id,
        actor_user_id=actor_user_id,
        action="deactivated",
        summary="Client deactivated.",
        payload_json=None,
    )
    return row


def activate_client_service(db, *, row, actor_user_id: int):
    row = repository.update_client(db, row, {"status": "active"})
    repository.write_audit_log(
        db,
        client_id=row.id,
        tenant_id=row.tenant_id,
        branch_id=row.branch_id,
        actor_user_id=actor_user_id,
        action="activated",
        summary="Client activated.",
        payload_json=None,
    )
    return row


def archive_client_service(db, *, row, actor_user_id: int):
    row = repository.update_client(db, row, {"status": "archived", "is_archived": True})
    repository.write_audit_log(
        db,
        client_id=row.id,
        tenant_id=row.tenant_id,
        branch_id=row.branch_id,
        actor_user_id=actor_user_id,
        action="archived",
        summary="Client archived.",
        payload_json=None,
    )
    return row


def restore_client_service(db, *, row, actor_user_id: int):
    row = repository.update_client(db, row, {"status": "active", "is_archived": False})
    repository.write_audit_log(
        db,
        client_id=row.id,
        tenant_id=row.tenant_id,
        branch_id=row.branch_id,
        actor_user_id=actor_user_id,
        action="restored",
        summary="Client restored from archive.",
        payload_json=None,
    )
    return row


def export_clients_csv(payload: dict) -> str:
    output = io.StringIO()
    writer = csv.writer(output)
    writer.writerow(
        [
            "client_code",
            "client_name",
            "client_type",
            "status",
            "pan",
            "gstin",
            "partner",
            "association_type",
            "association_source",
        ]
    )
    for row in payload.get("rows", []):
        writer.writerow(
            [
                row.get("client_code"),
                row.get("client_name"),
                row.get("client_type"),
                row.get("status"),
                row.get("pan"),
                row.get("gstin"),
                row.get("partner_name") or row.get("effective_partner_id"),
                row.get("association_type"),
                row.get("assoc_created_source"),
            ]
        )
    return output.getvalue()


def get_filter_options():
    return {
        "client_types": CLIENT_TYPES,
        "client_statuses": CLIENT_STATUS,
        "client_categories": CLIENT_CATEGORY_OPTIONS,
        "risk_categories": RISK_CATEGORIES,
    }


SELF_SERVICE_EDITABLE_FIELDS = {
    "client_name",
    "trade_name",
    "contact_person_name",
    "contact_person_designation",
    "mobile",
    "alternate_mobile",
    "email",
    "alternate_email",
    "address_line_1",
    "address_line_2",
    "city",
    "state",
    "pincode",
    "country",
    "notes",
}


def update_client_self_profile_service(db, *, row, data, current_user):
    payload = _payload_from_schema(data)
    payload = {key: value for key, value in payload.items() if key in SELF_SERVICE_EDITABLE_FIELDS}

    new_email = (payload.get("email") or "").strip().lower()
    if new_email:
        existing_user = repository.get_user_by_email(db, email=new_email, exclude_user_id=int(current_user.id))
        if existing_user:
            raise HTTPException(status_code=400, detail="That email is already used by another login.")

    updated = repository.update_client(db, row, payload)

    if new_email and new_email != (getattr(current_user, "email", "") or "").strip().lower():
        current_user.email = new_email
        db.add(current_user)
        db.commit()
        db.refresh(current_user)

    repository.write_audit_log(
        db,
        client_id=updated.id,
        tenant_id=updated.tenant_id,
        branch_id=updated.branch_id,
        actor_user_id=current_user.id,
        action="client_self_profile_updated",
        summary="Client updated own contact profile.",
        payload_json={"fields": sorted(payload.keys())},
    )
    return updated


def reset_client_portal_password_service(db, *, current_user, new_password: str):
    if len((new_password or "").strip()) < 8:
        raise HTTPException(status_code=400, detail="New password must be at least 8 characters.")
    current_user.password_hash = hash_password(new_password.strip())
    current_user.must_change_password = False
    db.add(current_user)
    db.commit()
    db.refresh(current_user)
    return current_user
